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

Styling — Tailwind, CSS Modules, component library আর design system

Styling React Apps — Tailwind CSS, CSS Modules, Component Libraries and Design Systems

Taught in Bangla, this lesson starts from CSS's global class names and compares four approaches: CSS Modules, Tailwind v4 with a real build and why dynamic class names fail, utilities, variants, themes and dark mode, clsx and variant maps against class soup, CSS-in-JS costs with Server Components, headless, styled and copy-in component libraries, design tokens for a small Bazarghar design system, and Bangla fonts, line height, focus and contrast.

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

  • Plain CSS, CSS Modules, Tailwind আর CSS-in-JS: কোনটির CSS কখন, কোথায় তৈরি হয়
  • Vite-এ Tailwind v4 সেটআপ, utility, responsive ও state variant, theme আর dark mode
  • clsx আর variant ম্যাপ দিয়ে “বাজারঘর”-এর Button বানানো
  • Radix, Base UI, shadcn/ui আর MUI-এর লেনদেন, মাপা bundle size সহ
  • Design token, বাংলা font ও line height, focus আর contrast ratio হিসাব

১ · সমস্যাটি নামের — চারটি পথ এক নজরে

একটি বড় বাজারের নোটিস বোর্ড ভাবুন। সব দোকানদার একই বোর্ডে নোটিস টাঙান। চালের দোকানি লিখলেন “কার্ড: আজ ছাড় নেই।” পাশের মোবাইল দোকানি লিখলেন “কার্ড: ৳৫০ রিচার্জ।” দুই “কার্ড” দুই জিনিস, কিন্তু বোর্ড একটাই। ক্রেতা কোনটি মানবেন?

CSS ঠিক এমন একটি বোর্ড। আপনি .card লিখলে সেটি পুরো পাতার সব class="card"-এ লাগে। এই সমস্যার নাম scope-এর অভাব। ছোট সাইটে টের পাওয়া যায় না। পর্ব ০৭-এ শেখা component ভাবনায় কিন্তু একই পাতায় অনেক লেখকের component বসে, তাই নামের ধাক্কা লাগে।

দুটি নিয়ম মিলে কে জিতবে তা ঠিক হয়। Cascade আর specificity. বিস্তারিত আছে HTML ও CSS কোর্সের cascade পর্বে, এখানে এক লাইনে: বেশি নির্দিষ্ট selector জেতে, সমান হলে যেটি পরে লেখা। ফলে বড় project-এ একটি ভয়ের চক্র তৈরি হয়। কেউ পুরোনো CSS মোছেন না, কারণ জানেন না কোথায় লাগছে। নতুন নিয়ম জেতাতে আরও লম্বা selector লেখেন, শেষে !important.

CSS কখন, কোথায় তৈরি হয়

Where each approach produces its CSS

চারটি styling পথ: build-এর সময় বনাম render-এর সময় build-এর সময় তৈরি, আলাদা .css ফাইল Plain CSS নিজের হাতে লেখা class নাম সবার, scope নেই CSS Modules হাতে লেখা CSS, নামে hash বসে .card হয় ._card_14suu_1 Tailwind (utility-first) ফাইল স্ক্যান, শুধু ব্যবহৃত utility একই class বারবার, CSS একই render-এর সময় তৈরি Runtime CSS-in-JS style থাকে JS-এর ভেতরে library-র কোডও bundle-এ যায় render-এর সময় <style> বসে
ছবি ২২.১ — ওপরের তিনটির ফল একটি CSS ফাইল। নিচেরটির কাজ চলে JavaScript চলার সময়।
পথScopeJS-এ বাড়তি খরচকোথায় মানায়
Plain CSSনেই, নিয়ম মেনে চলতে হয় (যেমন BEM)নেইছোট সাইট, global layout, reset
CSS Modulesফাইলভিত্তিক, build tool দেয়শুধু নামের ছোট ম্যাপহাতে CSS লিখতে চাওয়া দল
Tailwindনাম বানানোই লাগে নানেই (clsx খুব ছোট)দ্রুত UI, component-ভিত্তিক অ্যাপ
Runtime CSS-in-JScomponent-ভিত্তিকlibrary-র কোড, render-এর কাজপুরোনো codebase, prop-নির্ভর style

দুটি পাশের পথও চিনে রাখুন। BEM-এর মতো নামকরণ শুধু শৃঙ্খলা দিয়ে নামের ধাক্কা কমায়। Sass CSS লেখা সহজ করে, কিন্তু scope সমস্যা নিজে মেটায় না। আর React-এর style={{}} prop চলে শুধু সেই element-এ, তাতে :hover বা media query লেখা যায় না।

২ · CSS Modules — নামের পেছনে লুকানো পদবি

স্কুলের একই ক্লাসে দুজন “রাফি” থাকলে শিক্ষক পদবি জুড়ে ডাকেন। CSS Modules ঠিক এই কাজটি করে। আপনি ফাইলের ভেতরে ছোট নাম লেখেন, Vite build-এর সময় প্রতিটি নামের সাথে ফাইলভিত্তিক একটি hash জুড়ে দেয়। Import করলে পাওয়া যায় একটি object: আপনার নাম থেকে আসল নামের ম্যাপ।

import a from './Card.module.css'   // .card { padding: 1rem; ... }
import b from './Promo.module.css'  // .card { padding: 2rem; ... }

<div className={a.card}>চাল</div>
<div className={b.card}>অফার</div>

দুই ফাইলেই .card আছে। সত্যিকারের run-এ (Vite 8.3.5) build শেষে CSS আর JS-এ যা পাওয়া গেল:

._card_14suu_1{border-radius:12px;padding:1rem}
._title_14suu_2{color:teal}
._card_1ujsx_1{background:gold;padding:2rem}

{card:`_card_14suu_1`,title:`_title_14suu_2`}
{card:`_card_1ujsx_1`}

দুই .card আলাদা নাম পেয়েছে, তাই ধাক্কা লাগার পথ নেই। নিচের live block-এ Tailwind বা build tool নেই। তাই hash করা নাম হাতে লিখে একই ফল দেখানো হয়েছে, সাথে clsx-এর একটি ছোট্ট নকল। “অফার” বোতাম চেপে দেখুন class কীভাবে বদলায়।

<style>
  .Card_card_a1x { padding: 8px; background: #ecfdf5; }
  .Promo_card_b7k { padding: 8px; background: #fef3c7; }
  .Promo_hot_c2m { font-weight: 700; color: #b91c1c; }
</style>
<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const Card = { card: 'Card_card_a1x' };   // Card.module.css-এর ম্যাপ
const Promo = { card: 'Promo_card_b7k', hot: 'Promo_hot_c2m' };

function cx(...args) {                    // ছোট্ট clsx
  const out = [];
  for (const a of args) {
    if (typeof a === 'string' && a) out.push(a);
    else if (a && typeof a === 'object')
      for (const k in a) if (a[k]) out.push(k);
  }
  return out.join(' ');
}
function App() {
  const [hot, setHot] = useState(false);
  const cls = cx(Promo.card, false && 'hidden', { [Promo.hot]: hot });
  console.log('class:', cls);
  return (<><p className={Card.card}>চাল ৫ কেজি</p>
    <button className={cls} onClick={() => setHot(!hot)}>অফার</button></>);
}
createRoot(document.getElementById('root')).render(<App />);
</script>

চালাতেই ছাপা হয় class: Promo_card_b7k. false && 'hidden' ফেরত দেয় false, তাই cx সেটি বাদ দেয়। বোতাম চাপলে state বদলায়, আর ছাপা হয় class: Promo_card_b7k Promo_hot_c2m. আসল clsx এমনই, তবে array-ও সামলায়। Card_card_a1x-এর ছোঁয়া বোতামে লাগে না, কারণ নাম আলাদা।

সীমা: CSS Modules scope দেয়, নকশার শৃঙ্খলা দেয় না। দশটি ফাইলে দশ রকম সবুজ লেখা যায়। Global selector-এ যেতে হলে :global(...) লিখতে হয়। TypeScript-এ a.crad বানান ভুল ধরতে বাড়তি type সেটআপ লাগে।

৩ · Tailwind v4 — দর্জির মাপের বদলে তৈরি টুকরো, আর সত্যিকারের build

Plain CSS যেন প্রতিটি জামার জন্য দর্জিকে নতুন করে মাপ দেওয়া। Utility-first ধারণায় আগে থেকে ছোট ছোট টুকরো তৈরি থাকে: p-4 মানে চারদিকে ফাঁকা, text-lg মানে একটু বড় লেখা। আপনি নতুন নাম না ভেবে টুকরোগুলো জোড়া দেন। তুলনা আর Bootstrap-এর সাথে পার্থক্য আছে CSS framework পর্বে। এখানে আমরা React project-এ বসিয়ে দেখব।

লেখার সময়ে npm-এ tailwindcss ৪.৩.৩, vite ৮.৩.৫, react ১৯.৩.১. Tailwind-এর docs অনুযায়ী সেটআপ মাত্র তিন ধাপ। v4 (২০২৫ সালের জানুয়ারি) থেকে config-এর বড় অংশ CSS-এ চলে এসেছে, tailwind.config.js বাধ্যতামূলক নয়।

npm create vite@latest bazarghar -- --template react-ts
cd bazarghar
npm install
npm install tailwindcss @tailwindcss/vite clsx
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [react(), tailwindcss()],
})
@import "tailwindcss";

এবার “বাজারঘর”-এর পণ্যের কার্ড:

export function ProductCard({ name, price }: Props) {
  return (
    <article className="rounded-card bg-surface p-4 text-ink shadow dark:shadow-none md:p-6">
      <h3 className="font-bangla text-lg leading-bn font-semibold">{name}</h3>
      <p className="mt-1 text-brand-700 dark:text-brand-400">৳ {price}</p>
      <Button className="mt-3 w-full">কার্টে যোগ</Button>
    </article>
  )
}

এখানে bg-surface বা font-bangla Tailwind-এর নিজের class নয়, আমাদের theme থেকে আসে। অংশ ৮-এ দেখবেন। আগে দেখুন, Tailwind এই লেখা পড়ে CSS বানায় কীভাবে। সে কোনো class-এর তালিকা আগে থেকে পাঠায় না। ধাপে ধাপে দেখুন:

  1. ফাইল.tsxসাধারণ লেখা
  2. টুকরোtokensলেখা ভাঙা
  3. বাছাইmatchচেনা class
  4. CSS.cssদরকারি নিয়ম
ProductCard.tsxTailwind-এর কাছে শুধু লেখা
<article className="rounded-card bg-surface p-4
  text-ink shadow dark:shadow-none md:p-6">
পাওয়া টুকরোJSX বোঝে না, অক্ষর দেখে ভাঙে
article  className  rounded-card  bg-surface
p-4  text-ink  shadow  dark:shadow-none  md:p-6
বাছাইচেনা utility থাকে, বাকিটা বাদ
article    → utility নয়, বাদ
className  → utility নয়, বাদ
p-4        → আছে
md:p-6     → আছে (variant md + p-6)
p-4সত্যিকারের build থেকে
.p-4{padding:calc(var(--spacing) * 4)}
/* theme-এ --spacing:.25rem */
/* .25rem × 4 = 1rem = 16px */
md:p-6variant নিয়মটিকে মুড়ে দেয়
@media (width>=48rem){
  .md\:p-6{padding:calc(var(--spacing) * 6)}
}
১০০টি নতুন componentএকই class, সত্যিকারের run
আগে : index-BmBz4Doq.css  11.11 kB
পরে : index-BmBz4Doq.css  11.11 kB
নামের hash একই, মানে ফাইল হুবহু এক
Badge.tsxফাঁদ: লেখায় পুরো নাম নেই
className={`bg-${color}-600 text-white`}
লেখায় “bg-green-600” কোথাও নেই
CSS-এ bg-green-600: 0 বার
  1. ধাপ ১ — সাধারণ লেখা। Tailwind আপনার ফাইলকে code হিসেবে চালায় না, বোঝেও না। তার কাছে এটি একটি লম্বা লেখা, দর্জির খাতার পাতার মতো।
  2. ধাপ ২ — টুকরো করা। Rust-এ লেখা স্ক্যানার (@tailwindcss/oxide প্যাকেজ) class-এর মতো দেখতে সব টুকরো তুলে নেয়। article আর className-ও ওঠে, কারণ সে JSX বোঝে না।
  3. ধাপ ৩ — বাছাই। যে টুকরো চেনা utility নয়, সেটি চুপচাপ বাদ। p-4 চেনা। md:p-6-ও চেনা: variant md আর utility p-6.
  4. ধাপ ৪ — CSS তৈরি। p-4 থেকে নিয়ম হলো। Spacing-এর একক .25rem, তাই 4 গুণ মানে 1rem, সাধারণ browser-এ 16px.
  5. ধাপ ৫ — Variant. md: নিয়মটিকে media query-তে মুড়ে দেয়, 48rem বা তার বেশি চওড়া পর্দায় 24px ফাঁকা। Class নামের : CSS-এ \: হয়ে escape হয়।
  6. ধাপ ৬ — একটিই নিয়ম। একই class হাজারবার পেলেও CSS-এ নিয়ম একবার। আমরা একই class-এর ১০০টি component যোগ করেছি। CSS ফাইলের নাম আর আকার বদলায়নি। বেড়েছে শুধু JS, কারণ markup তো JS-এই আছে।
  7. ধাপ ৭ — ফাঁদ। `bg-${color}-600` লিখলে স্ক্যানার পুরো নামটি লেখায় পায় না। Browser-এ class ঠিকই বসে, কিন্তু তার কোনো CSS নেই। সমাধান: দুটি পুরো নাম লিখুন, color === 'green' ? 'bg-green-600 text-white' : 'bg-amber-600 text-white'. এরপর দুটি নিয়মই তৈরি হয়েছে।

সত্যিকারের npm run build (Tailwind ৪.৩.৩, Vite ৮.৩.৫, একটি লোকাল run, ~/bazarghar):

$ npm run build
> tsc -b && vite build
✓ 19 modules transformed.
dist/index.html                   0.45 kB │ gzip:  0.29 kB
dist/assets/index-BmBz4Doq.css   11.11 kB │ gzip:  2.96 kB
dist/assets/index-4uBs9cUv.js   221.09 kB │ gzip: 69.38 kB
✓ built in 113ms
অবস্থা (একই project)CSSgzip
কোনো স্ক্যান নেই, source(none)৪.১৯ kB১.৪০ kB
বাজারঘরের তিনটি component (ওপরের run)১১.১১ kB২.৯৬ kB
একই class-এর ১০০টি component বাড়তি১১.১১ kB২.৯৬ kB
একটি নতুন utility underline১১.১৫ kB২.৯৭ kB
দুটি SVG ফোল্ডার স্ক্যান থেকে বাদ৯.৪৩ kB২.৬৯ kB

শেষ সারিটি মজার। আমাদের CSS-এ .transform আর .filter নিয়ম পাওয়া গেল, অথচ কোনো component-এ এগুলো নেই। Vite-এর template-এর SVG ফাইলে ঐ শব্দ দুটি আছে, স্ক্যানার সেখান থেকে পেয়েছে। এটাই “সাধারণ লেখা” স্ক্যানের প্রমাণ। সাথে এদের সঙ্গী variable-গুলোও এসেছিল। বাদ দিতে পারেন এভাবে:

@import "tailwindcss";
@source not "../public";
@source not "./assets";

একটি হিসাব মনে রাখুন। gzip-এর পর ২.৯৬ kB CSS, আর ঐ একই build-এ JS ৬৯.৩৮ kB. বেশিরভাগ React অ্যাপে bundle-এর ভার JS-এ, তাই CSS ফাইলের সামান্য কমবেশি নিয়ে বেশি ভাবার আগে JS মাপুন। .gitignore-এ থাকা ফাইল, node_modules, ছবির মতো binary ফাইল আর CSS ফাইল স্ক্যান হয় না। কোনো npm প্যাকেজের ভেতরের class লাগলে @source দিয়ে যোগ করতে হয়।

৪ · Utility, variant, theme আর dark mode

Tailwind-এর শক্তি utility-তে যতটা, variant-এ তার চেয়ে বেশি। Variant হলো class-এর আগে বসা শর্ত। মূল CSS জানা থাকলে নিচের সারণি মিলিয়ে নিতে পারবেন। ডান দিকের CSS আমাদের build থেকে নেওয়া, সামান্য ছাঁটা।

লিখলেনঅর্থতৈরি CSS
hover:bg-action-hoverমাউস রাখলে@media (hover:hover){…:hover{…}}
focus-visible:outline-2keyboard focus-এ…:focus-visible{outline-width:2px}
disabled:opacity-50বোতাম বন্ধ থাকলে…:disabled{opacity:.5}
sm:grid-cols-240rem বা বড় পর্দায়@media (width>=40rem){…}
lg:grid-cols-364rem বা বড় পর্দায়@media (width>=64rem){…}

দুটি জিনিস খেয়াল করুন। এক, hover: v4-এ @media (hover:hover)-এর ভেতরে থাকে, তাই ফোনে আঙুল ছোঁয়ালে hover আটকে থাকে না। দুই, Tailwind mobile-first: p-4 সব পর্দায়, md:p-6 শুধু বড় থেকে শুরু। md: মানে “শুধু ট্যাবলেট” নয়। Breakpoint-এর ধারণা responsive পর্বে আছে।

Dark mode দুই পথে

ডিফল্টে dark: চলে prefers-color-scheme দিয়ে, অর্থাৎ ব্যবহারকারীর OS-এর পছন্দে। নিজের বোতাম দিয়ে বদলাতে হলে variant-টি নতুন করে লিখতে হয়। ধারণাটি dark mode পর্বে আছে, এখানে Tailwind-এর লেখা:

@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));

তৈরি CSS ছিল .dark\:text-brand-400:where([data-theme=dark],[data-theme=dark] *). :where()-এর specificity শূন্য, তাই এটি নিয়ম জটিল করে না। তবে প্রতিটি element-এ dark: লেখার চেয়ে ভালো পথ আছে: CSS variable বদলানো। একটি attribute বদলালেই সব রং বদলে যায়, React-কে কিছুই জানাতে হয় না। নিচের ব্লক চালিয়ে বোতামটি দুবার চাপুন।

<style>
  :root { --bg: #ffffff; --ink: #1f2937; --action: #0f766e; --on-action: #ffffff; }
  [data-theme=dark] { --bg: #111827; --ink: #f3f4f6;
                      --action: #2dd4bf; --on-action: #042f2e; }
  .card { background: var(--bg); color: var(--ink); padding: 12px; }
  .btn { background: var(--action); color: var(--on-action);
         padding: 6px 12px; border: 0; border-radius: 8px; }
</style>
<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
let renders = 0;
function toggle() {
  const root = document.documentElement;
  root.dataset.theme = root.dataset.theme === 'dark' ? 'light' : 'dark';
  const bg = getComputedStyle(root).getPropertyValue('--bg').trim();
  console.log(root.dataset.theme, '--bg =', bg, '| renders =', renders);
}
function Card() {
  renders += 1;
  console.log('Card render:', renders);
  return (
    <div className="card">
      <p>মসুর ডাল ১ কেজি</p>
      <button className="btn" onClick={toggle}>থিম বদলান</button>
    </div>
  );
}
createRoot(document.getElementById('root')).render(<Card />);
</script>

Edge-এ চালিয়ে দুবার চাপলে সত্যিকারের আউটপুট:

Card render: 1
dark --bg = #111827 | renders = 1
light --bg = #ffffff | renders = 1

রং বদলেছে, কিন্তু renders ১-ই আছে। কাজটি সেরেছে browser-এর CSS engine. পর্ব ১০-এর state দিয়ে থিম রাখলে পুরো গাছ re-render হতে পারত। বাস্তবে পছন্দটি localStorage-এ রাখা হয়, আর পাতা আঁকার আগে <head>-এর ছোট script থেকে attribute বসানো হয়। না হলে dark-এর ব্যবহারকারী এক ঝলক সাদা পাতা দেখেন।

আরও গভীরে: Tailwind-এর CSS থাকে native cascade layer-এ, ক্রমটি theme, base, components, utilities. আর CSS-এর নিয়মে layer-এর বাইরের সাধারণ CSS সব layer-কে হারায়, specificity যাই হোক। আমাদের run-এ index.css-এ লেখা .promo { padding: 2rem } একই element-এর p-4-কে হারিয়েছে, Edge-এ মাপা padding 32px. তাই পুরোনো CSS-এর সাথে Tailwind মেশালে utility “কাজ না করা” দেখলে আগে layer দেখুন। নিজের CSS @layer components { … }-এ রাখলে utility আবার জিতবে।

৫ · clsx, variant ম্যাপ আর class soup-এর ওষুধ

Tailwind-এর সবচেয়ে বড় অভিযোগ class soup: এক লাইনে পনেরো-বিশটি class, পড়া কঠিন। দোকানের হিসাবে যদি প্রতিবার “চাল + ডাল + তেল + লবণ” আলাদা লিখতে হয়, দোকানি একটি “মাসিক প্যাকেট” বানান। লিখতে হয় একবার, ডাকেন নাম ধরে। React-এ সেই প্যাকেট হলো পর্ব ০৯-এর component.

Class-গুলো শর্ত অনুযায়ী জুড়তে clsx লাগে। ছোট একটি function, string, object, array নেয়, মিথ্যা মান বাদ দেয়। আর Tailwind-এর নিয়ম অনুযায়ী পুরো class নাম একটি ম্যাপে রাখতে হয়।

import clsx from 'clsx'
import type { ComponentProps } from 'react'

const variants = {
  primary: 'bg-action text-on-action hover:bg-action-hover',
  ghost: 'bg-transparent text-ink hover:bg-ink/10',
  danger: 'bg-red-700 text-white hover:bg-red-800',
}
const sizes = { sm: 'px-3 py-1 text-sm', md: 'px-4 py-2 text-base' }

type Props = ComponentProps<'button'> & {
  variant?: keyof typeof variants
  size?: keyof typeof sizes
}

export function Button({ variant = 'primary', size = 'md', className, ...rest }: Props) {
  return (
    <button
      className={clsx(
        'rounded-card font-bangla focus-visible:outline-2 focus-visible:outline-offset-2',
        'disabled:opacity-50',
        variants[variant],
        sizes[size],
        className,
      )}
      {...rest}
    />
  )
}

এখন component-এর ব্যবহারকারী শুধু লেখেন <Button variant="danger" size="sm">. কীভাবে সেটি class হয়, ধাপে ধাপে দেখুন। সহজ রাখতে শুরুর class একটিই দেখানো হয়েছে।

1function Button({ variant, size, className }) {2  const v = variants[variant];3  const s = sizes[size];4  const cls = clsx('rounded-card', v, s, className);5  return <button className={cls} />;6}
মানগুলো (Watch)
variant
size
className
v
s
  1. ডাক এলো। <Button variant="danger" size="sm" className="w-full" />. তিনটি prop পৌঁছাল, এখনো কোনো class তৈরি হয়নি।
  2. Variant খোঁজা। variants['danger'] থেকে পুরো লেখাটি এলো। এই পুরো লেখা ফাইলেই আছে, তাই স্ক্যানার তিনটি class-ই চেনে।
  3. Size খোঁজা। sizes['sm'] থেকে এলো px-3 py-1 text-sm. চাইলে TypeScript size="xl" লেখা আটকে দেয়, কারণ prop-এর type ম্যাপের key থেকে আসে।
  4. clsx জোড়া দিল। চারটি টুকরো এক লাইনে হলো। Component-এর ব্যবহারকারীর w-full সবার শেষে বসল।
  5. DOM-এ গেল। React class-টি বোতামে বসাল। CSS আগে থেকেই তৈরি, চলার সময় কোনো CSS বানাতে হলো না।
  6. আরেকটি ডাক: <Button variant="ghost" />. size না দেওয়ায় ডিফল্ট md, আর className undefined বলে clsx সেটি বাদ দিল।

এবার একই ধারণা আপনার হাতে। Live block-এ Tailwind নেই, তাই ওপরে হাতে লেখা কয়েকটি ছোট class রাখা হয়েছে। variant বদলে দেখুন, আর Tab চেপে বোতামের focus দাগ দেখুন।

<style>
  .btn { border: 0; border-radius: 8px; margin: 4px; }
  .btn:focus-visible { outline: 2px solid #1f2937; outline-offset: 2px; }
  .btn:disabled { opacity: .5; }
  .sm { padding: 4px 12px; } .md { padding: 8px 16px; }
  .action { background: #0f766e; color: #fff; }
  .ghost { background: transparent; color: #1f2937; }
  .danger { background: #b91c1c; color: #fff; }
</style>
<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const variants = { primary: 'action', ghost: 'ghost', danger: 'danger' };
const cx = (...a) => a.filter(Boolean).join(' ');

function Button({ variant = 'primary', size = 'md', className, ...rest }) {
  const cls = cx('btn', variants[variant], size, className);
  console.log(variant, size, '=>', cls);
  return <button className={cls} {...rest} />;
}
function App() {
  return (<>
    <Button>কার্টে যোগ</Button>
    <Button variant="ghost" size="sm">বিস্তারিত</Button>
    <Button variant="danger" className="wide" disabled>বাতিল</Button>
  </>);
}
createRoot(document.getElementById('root')).render(<App />);
</script>

চালালে তিনটি লাইন আসে: primary md => btn action md, ghost sm => btn ghost sm আর danger md => btn danger md wide. বাকি prop, যেমন disabled, ...rest দিয়ে আসল বোতামে গেছে। তাই component-টি একটি সাধারণ <button>-এর সব ক্ষমতা রাখে।

বাইরে থেকে class দিলে কে জেতে?

এখানে একটি গোপন ফাঁদ। Class লেখার ক্রম CSS-এ কিছু ঠিক করে না, ঠিক করে CSS ফাইলের ক্রম। Tailwind নিজের ক্রমে নিয়ম সাজায়। Edge-এ বাজারঘরের Button মেপে পাওয়া গেল (ডিফল্টে px-4, মানে 16px):

<Button className="px-8">  padding-left: 32px   (কাজ করল)
<Button className="px-2">  padding-left: 16px   (কাজ করল না!)

দুটোতেই class-এ শেষে বসেছে, তবু px-2 হেরেছে। কারণ CSS-এ .px-2 আগে, .px-4 পরে। সমাধান tailwind-merge: এটি একই কাজের আগের class ফেলে দেয়। সত্যিকারের run (tailwind-merge ৩.৭.০):

clsx('px-4 py-2', 'px-2')                 → px-4 py-2 px-2
twMerge('px-4 py-2', 'px-2')              → py-2 px-2
twMerge('bg-brand-600 hover:bg-brand-700', 'bg-red-700')
                                          → hover:bg-brand-700 bg-red-700

দুটি প্রচলিত পথ আছে। clsx আর twMerge জুড়ে একটি cn() function, আর variant ম্যাপ লেখার জন্য cva. ছোট project-এ এর কোনোটিই বাধ্যতামূলক নয়। Variant যত বেশি বদলাবে, className খোলা রাখা তত কম দরকার। এমন বাইরের ফাঁকফোকর যত কম, design system তত ঠিক থাকে। পর্ব ০৯-এর boolean prop বিস্ফোরণ এখানেও মনে রাখুন: isRed isBig isGhost-এর বদলে একটি variant.

@apply দিয়ে লুকাবেন না। CSS ফাইলে .btn { @apply px-4 py-2 … } লেখা যায়। বেশি ব্যবহারে আবার নতুন নাম আর পুরোনো CSS-এর সব সমস্যা ফিরে আসে। পুনরাবৃত্তির প্রথম ওষুধ component, CSS-এর নতুন class নয়।

৬ · CSS-in-JS — সুবিধা, চলার সময়ের খরচ আর Server Component

CSS-in-JS-এ style লেখা হয় JavaScript-এর ভেতরে। Prop অনুযায়ী রং বদলানো খুব সহজ, আর component মুছলে তার style-ও মুছে যায়। ২০১০-এর দশকের শেষে React জগতে এটি খুব জনপ্রিয় ছিল।

import styled from '@emotion/styled'
const Btn = styled.button`background:#0f766e;color:#fff;`

Runtime ধরনের library-তে দাম দিতে হয় তিন জায়গায়। Library-র কোড bundle-এ যায়। Render-এর সময় style হিসাব হয়ে <style>-এ বসে। আর server-rendering-এ style জোগাড় করে HTML-এ ঢোকাতে আলাদা সেটআপ লাগে। একটি বোতামের সবচেয়ে সরল build-এ মাপা ফল আছে অংশ ৭-এর সারণিতে।

তারপর আসে Server Component, যা পর্ব ৫২-এ বিস্তারিত দেখবেন। এখানে একটি কথাই দরকার: Server Component browser-এ চলে না, তাই সেখানে React Context-ও চলে না। Runtime CSS-in-JS library-গুলো theme-এর জন্য সাধারণত Context-এর ওপর নির্ভর করে। AI প্রায়ই বলে “CSS-in-JS Server Component-এ চলে না।” পুরো সত্য কী? Next.js ১৬.৪.৩-এ একই পাতা বানিয়ে সত্যিকারের next build চালানো হলো (আউটপুট ছাঁটা):

# @emotion/styled 11.14.1, 'use client' ছাড়া
Collecting page data using 4 workers ...
Error: Failed to collect configuration for /
  [cause]: TypeError: g.createContext is not a function
> Build error occurred

# একই ফাইলের প্রথমে 'use client' যোগ করে
✓ Generating static pages using 4 workers (3/3)
○  (Static)  prerendered as static content

# styled-components 6.6.0, 'use client' ছাড়া
✓ Generating static pages using 4 workers (3/3)
○  (Static)  prerendered as static content

অর্থাৎ ছবিটি এক রকম নয়। Emotion-এর এই পাতা Client Component হলে তবেই build হলো। styled-components v6 Server Component-এ build হলো, আর তৈরি HTML-এ দেখা গেল <style data-styled="">.dLCdml{background:#0f766e;color:#fff;}</style>. এটি একটি ছোট পরীক্ষা, সব feature-এর প্রমাণ নয়। Library বদলায়, তাই নিজের version-এ চালিয়ে দেখুন।

২০২৫ সালের মার্চে styled-components-এর maintainer প্রকল্পটিকে maintenance mode-এ নেওয়ার ঘোষণা দেন, নতুন project-এ না নেওয়ার পরামর্শসহ। পরে নতুন release এসেছে, তাই অবস্থা দেখে নিতে হয়। এর পাশে zero-runtime পথও আছে: Next.js-এর docs vanilla-extract, Panda CSS আর StyleX-এর নাম তালিকায় রেখেছে। এগুলো JS-এর মতো করে style লিখতে দিলেও মূল কাজ build-এর সময় করার দিকে যায়। নতুন project-এ অনেক দল এখন CSS Modules বা Tailwind বেছে নেন, তবে পুরোনো CSS-in-JS codebase তাড়াহুড়ো করে পাল্টানোর যুক্তি কম। আগে মাপুন।

৭ · Component library — headless, styled আর কপি করা কোড

Dialog, dropdown, tab বানানো শুনতে সহজ, আসলে কঠিন। একটি Dialog-কে Esc-এ বন্ধ হতে হয়। Focus ভেতরে আটকে রাখতে হয়, বন্ধ হলে আগের জায়গায় ফেরাতে হয়। Screen reader-কে ARIA দিয়ে জানাতে হয়। এই কাজগুলো হাতে লিখলে সাধারণত ভুল থাকে। তাই component library. নতুন বাড়ি তোলার তিন উপায় ভাবুন: শুধু কাঠামো ঢালাই করা ঘর কেনা, পুরো সাজানো ফ্ল্যাট কেনা, অথবা নকশাসহ মিস্ত্রি ডেকে নিজের জমিতে বানানো।

Headless

Headless library আচরণ আর accessibility দেয়, চেহারা নয়। Style সম্পূর্ণ আপনার। ঢালাই করা ঘর।

উদাহরণ: Radix, Base UI

Styled

চেহারাসহ তৈরি। দ্রুত শুরু, বিশেষ করে admin panel-এ। নিজের ব্র্যান্ডে আনতে theme বা override লাগে। সাজানো ফ্ল্যাট।

উদাহরণ: MUI

কোড কপি

shadcn/ui নিজেকে library নয় বলে। CLI কোড আপনার project-এ বসায়, ফাইল এখন আপনার। মিস্ত্রি আর নকশা।

headless-এর ওপর Tailwind

Bundle-এ দাম কত? একই React-এ প্রতিটি আলাদা Vite build, সবচেয়ে ছোট ব্যবহার। এটি একটি লোকাল run, সাধারণ নিয়ম নয়। Library-র বেশি অংশ ব্যবহার করলে খরচ বাড়ে, আর একবার দাম দিলে পরের component কম লাগে। Headless দুটিতে style একদম ছিল না, সেটি আলাদা।

Build (লেখার সময়ের version)JSgzipবাড়তি gzip
শুধু React, সাধারণ <button>২১৯.৪৯ kB৬৮.৬২ kB—
@emotion/styled ১১.১৪ বোতাম২৪০.৯৬ kB৭৭.৫৬ kB৮.৯৪ kB
styled-components ৬.৬ বোতাম২৪৬.০৩ kB৭৯.২৬ kB১০.৬৪ kB
radix-ui ১.৮ Dialog২৫৯.১২ kB৮১.৬১ kB১২.৯৯ kB
@base-ui/react ১.৯ Dialog২৮০.৯৯ kB৮৮.৮৮ kB২০.২৬ kB
@mui/material ৯.৫ Button৩২৬.৩৬ kB১০৪.৫১ kB৩৫.৮৯ kB
@mui/material ৯.৫ Button + Dialog৩৫৪.২৯ kB১১২.৯১ kB৪৪.২৯ kB

হিসাব: MUI বোতামের বাড়তি ৩৫.৮৯ kB নামাতে কত সময়? ধরা যাক গতি ১ Mbps, এটি অনুমান করা মান। ৩৫.৮৯ × ৮ = ২৮৭.১২ kilobit. ২৮৭.১২ ÷ ১০০০ ≈ ০.২৯ সেকেন্ড। ৫ Mbps ধরলে ≈ ০.০৬ সেকেন্ড। এটি শুধু নামানোর সময়, JS parse আর চালানোর সময় আলাদা। এই সংখ্যা MUI-কে খারাপ প্রমাণ করে না। অনেক screen-এর admin panel-এ MUI কয়েক সপ্তাহের কাজ বাঁচাতে পারে, সেখানে এই দাম ন্যায্য। খোলা দোকানের প্রথম পাতায় হিসাব আলাদা হতে পারে।

shadcn/ui-এর সত্যিকারের run

লেখার সময়ে CLI ৪.২৩.০. একটি কপি project-এ চালানো হলো। লক্ষ্য করুন, এটি node_modules নয়, আপনার src-এ ফাইল লেখে:

$ npx shadcn@latest init -t vite -b base -p nova --no-monorepo -y
✔ Writing components.json.
✔ Installing dependencies.
✔ Created 2 files:
  - src\components\ui\button.tsx
  - src\lib\utils.ts
✔ Updating src\index.css

$ npx shadcn@latest add button dialog -y
✔ Created 1 file:
  - src\components\ui\dialog.tsx

$ wc -l src/components/ui/*
   57 src/components/ui/button.tsx
  158 src/components/ui/dialog.tsx

তৈরি button.tsx-এর প্রথম লাইনগুলোতে ছিল @base-ui/react/button আর cva. অর্থাৎ অংশ ৫-এর ধারণাই, অনেক বেশি variant-সহ। সাথে package.json-এ lucide-react, tw-animate-css, একটি font প্যাকেজ আর cn-সহ কয়েকটি dependency যোগ হলো। Changelog অনুযায়ী এখন Radix আর Base UI দুটোই বেছে নেওয়া যায়, Base UI ২০২৬ সালে ডিফল্টের দিকে গেছে। এই তথ্য দ্রুত বদলায়, তাই docs দেখে নিন।

প্রশ্নHeadlessStyledকোড কপি
শুরুর গতিধীর, চেহারা নিজেসবচেয়ে দ্রুতদ্রুত
নিজের ব্র্যান্ডপূর্ণ স্বাধীনতাtheme-এর সীমায়কোড নিজের, পূর্ণ
আপডেট আর bug fixnpm updatenpm update, মাঝে মাঝে বড় migrationকপি করা কোড নিজে মেলাতে হয়
ঝুঁকিনকশার কাজ বেশিlock-in, bundleকোডের মালিকানা, পুরোনো কপি

কোনো একটি সব জায়গায় সেরা নয়, এখানে সমাজে মতভেদও আছে। সিদ্ধান্তের একটি সহজ ক্রম: ডিজাইনার আর নিজের ব্র্যান্ড জরুরি হলে headless. ভেতরের admin টুলে styled. Tailwind-ভিত্তিক অ্যাপ আর কোড পড়তে পারা দল থাকলে কপি করা পথ ভেবে দেখুন।

৮ · Design token আর বাজারঘরের ছোট design system

বাজারঘরের সবুজটি যদি ৪০টি ফাইলে #0f766e হয়ে লেখা থাকে, রং বদলানো মানে ৪০টি ফাইল ঘাঁটা। ব্যাংকের রেট বোর্ড ভাবুন: সব শাখা এক জায়গার রেট দেখায়, প্রতিটি শাখা আলাদা করে হিসাব করে না। Design token তেমন নাম-দেওয়া একটি নকশার সিদ্ধান্ত: রং, ফাঁকা, ফন্ট, কোণের বাঁক। Design system এর ওপরে বসানো component আর নিয়মের সংগ্রহ।

বাজারঘরের design system-এর স্তর

Tokens flow up into components and pages

Primitive token, semantic token, component আর পাতা পাতা: পণ্যের তালিকা, কার্ট শুধু component জোড়া দেয় Component: Button, ProductCard variant ম্যাপ, focus, disabled Semantic token: --action অর্থ, থিমে মান বদলায় Primitive token: --color-brand-600 কাঁচা মান: #0f766e
ছবি ২২.২ — নিচের স্তরে একবার বদলালে ওপরের সব জায়গায় পৌঁছায়। পাতা কখনো কাঁচা রং দেখে না।

Tailwind v4-এ token লেখা হয় @theme-এ। Variable-এর নামের প্রথম অংশ (--color-*, --font-*, --radius-*, --leading-*) ঠিক করে কোন utility তৈরি হবে। বাজারঘরের আসল ফাইল, যা ওপরের সব run-এ চলেছে:

@import "tailwindcss";

/* dark: চলবে <html data-theme="dark"> থাকলে */
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));

/* ধাপ ১: কাঁচা মান (primitive token) */
@theme {
  --color-brand-400: #2dd4bf;
  --color-brand-600: #0f766e;
  --color-brand-700: #115e59;
  --font-bangla: "Noto Sans Bengali", "Hind Siliguri", sans-serif;
  --radius-card: 0.75rem;
  --leading-bn: 1.7;
}

/* ধাপ ২: অর্থ (semantic token), utility বানায় */
@theme inline {
  --color-surface: var(--surface);
  --color-ink: var(--ink);
  --color-action: var(--action);
  --color-action-hover: var(--action-hover);
  --color-on-action: var(--on-action);
}

/* ধাপ ৩: থিম অনুযায়ী মান */
:root {
  --surface: #ffffff; --ink: #1f2937;
  --action: var(--color-brand-600);
  --action-hover: var(--color-brand-700);
  --on-action: #ffffff;
}
[data-theme=dark] {
  --surface: #111827; --ink: #f3f4f6;
  --action: var(--color-brand-400);
  --action-hover: #5eead4;
  --on-action: #042f2e;
}

@theme utility বানায়, :root বানায় না, সে কারণেই দুটি আলাদা। inline থাকলে utility সরাসরি var(--action) ব্যবহার করে, তাই পাতার যেকোনো অংশে data-theme বসালেও সেখানকার মানই কাজ করে। এবার একটি token-এর যাত্রা দেখুন:

  1. Primitivebrand-600কাঁচা মান
  2. Semantic--actionঅর্থ
  3. Utilitybg-actionclass
  4. পর্দাrgb()আসল রং
@themeএকবারই লেখা
--color-brand-600: #0f766e;
:rootঅর্থ জুড়ে দেওয়া
--action: var(--color-brand-600);
--on-action: #ffffff;
তৈরি CSSসত্যিকারের build থেকে
.bg-action{background-color:var(--action)}
Button.tsxcomponent শুধু নাম জানে
primary: 'bg-action text-on-action
          hover:bg-action-hover'
Edge, lightgetComputedStyle
background-color: rgb(15, 118, 110)
color:            rgb(255, 255, 255)
Edge, darkdata-theme="dark" বসিয়ে
background-color: rgb(45, 212, 191)
color:            rgb(4, 47, 46)
  1. কাঁচা মান। --color-brand-600 শুধু একটি রং। নামে “কাজ” নেই, শুধু “কোন রং” আছে।
  2. অর্থ। --action বলে “মূল কাজের বোতামের রং”। সাথে জোড়া --on-action, “তার ওপরের লেখার রং”। রং সবসময় জোড়ায় ভাবতে হয়।
  3. Utility. @theme inline-এর --color-action থেকে .bg-action তৈরি হলো। ভেতরে আছে var(--action), কোনো রঙের মান নয়।
  4. Component. Button-এর primary variant শুধু bg-action নাম জানে। সে জানে না সবুজ কত।
  5. পর্দা, light. Browser variable মিলিয়ে আসল রং বের করল: #0f766e মানে rgb(15, 118, 110). লেখা সাদা।
  6. পর্দা, dark. [data-theme=dark] এলে --action হলো brand-400, --on-action হলো গাঢ় #042f2e. Component-এর একটি অক্ষরও বদলায়নি। ব্র্যান্ডের রং কাল বদলালে শুধু ধাপ ১-এর লাইন বদলাবে।

বাজারঘরের ছোট নিয়মপত্র এটুকুই: কাঁচা hex শুধু index.css-এ। পাতা শুধু component ডাকে। Variant হিসেবে primary, ghost, danger, মাপ sm, md. প্রতিটি রং জোড়া contrast মাপা। ছোট দলের জন্য এই একটি ফাইলই design system-এর শুরু, বড় ডকুমেন্টেশন সাইট নয়। Figma-র মতো টুল আর code-এর মধ্যে token ভাগ করতে একটি সাধারণ ফরম্যাট আছে: Design Tokens Community Group-এর প্রথম স্থির সংস্করণ এসেছে ২০২৫ সালের অক্টোবরে।

৯ · বাংলা UI আর accessibility — font, line height, focus, contrast

ইংরেজির জন্য বানানো design system বাংলায় হুবহু খাটে না। বাংলা অক্ষরের ওপরে মাত্রা, নিচে উ-কার, ঋ-কার আর যুক্তাক্ষর থাকে। Tailwind-এর text-sm-এর সঙ্গী line height প্রায় ১.৪৩ (calc(1.25 / .875)), text-base-এর ১.৫, আর text-2xl-এর প্রায় ১.৩৩. ইংরেজিতে ঠিক দেখায়, বাংলায় লাইন গায়ে গায়ে লেগে যায়। ভিত্তি আছে ওয়েবে বাংলা পর্বে, UI-এর জন্য কয়েকটি কাজের নিয়ম:

  • Line height: মূল লেখায় ১.৬ থেকে ১.৮. তাই আমাদের --leading-bn: 1.7 আর leading-bn. বোতাম আর ছোট লেবেলেও একবার চোখে দেখে নিন।
  • Font: --font-bangla-এ একটি fallback তালিকা রাখুন। font-display: swap আর শুধু bengali subset নিলে দেরি আর লাফানো কমে। বিস্তারিত পর্ব ৫৯-এ।
  • মাপ: text-xs বাংলায় প্রায়ই পড়া কঠিন। একই px-এ বাংলা ইংরেজির চেয়ে ঘন দেখায়।
  • Letter spacing আর italic: ইংরেজি শিরোনামের tracking-wide বা italic অভ্যাস বাংলায় আনবেন না। ফাঁক দিলে মাত্রা ভেঙে দেখায়।
  • Button height: h-8-এর মতো স্থির উচ্চতায় দুই লাইনের বাংলা কেটে যেতে পারে। Padding দিয়ে উচ্চতা হতে দিন। আমাদের Button তাই করে।

Focus — keyboard-এর পথ

অনেকে মাউস ব্যবহার করেন না। তাঁদের একমাত্র পথনির্দেশ focus-এর দাগ। WCAG ২.২-এর 2.4.7 বলে, keyboard focus-এর চিহ্ন দেখা যেতে হবে। AI প্রায়ই outline-none লেখে “সুন্দর দেখানোর” জন্য, পাশে বদলি কিছু দেয় না। নিয়ম হলো focus-visible দিয়ে বদলি দাগ রাখা, যেমন আমাদের focus-visible:outline-2 focus-visible:outline-offset-2. ধারণাটি অ্যাক্সেসিবল CSS পর্বে আছে। বোতাম হলে <div onClick> নয়, আসল <button> লিখুন।

Contrast ratio — হাতে হিসাব

লেখা আর পটের আলোর পার্থক্য মাপে contrast ratio. WCAG-এর AA স্তরে সাধারণ লেখার জন্য অন্তত 4.5:1, বড় লেখা (১৮pt, বা ১৪pt মোটা) আর বোতামের সীমার মতো UI অংশে অন্তত 3:1. সূত্র:

Contrast = (L১ + 0.05) ÷ (L২ + 0.05)
L১ = বেশি উজ্জ্বল রং, L২ = কম উজ্জ্বল রং

L হলো relative luminance. প্রতিটি চ্যানেল c = মান ÷ 255. c ≤ 0.04045 হলে c ÷ 12.92, নইলে ((c + 0.055) ÷ 1.055)2.4. তারপর L = 0.2126R + 0.7152G + 0.0722B. চোখ সবুজে সবচেয়ে সংবেদনশীল, তাই G-এর ভাগ সবচেয়ে বড়।

উদাহরণ: সাদার ওপর #0f766e, মানে rgb(15, 118, 110). রূপান্তরের পর R = 0.0048, G = 0.1812, B = 0.1559. তাহলে L = 0.2126 × 0.0048 + 0.7152 × 0.1812 + 0.0722 × 0.1559 = 0.0010 + 0.1296 + 0.0113 = 0.1418. সাদার L = 1. Contrast = 1.05 ÷ 0.1918 ≈ 5.47. সাধারণ লেখায় 4.5 পার হলো। নিচের calculator-এ এটি মিলিয়ে নিন, আর নিজের রং বসান।

// WCAG 2.2: sRGB মান থেকে relative luminance
function channel(v) {
  const c = v / 255;
  return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
}
function luminance(hex) {
  const n = parseInt(hex.slice(1), 16);
  const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255;
  return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
}
function contrast(a, b) {
  const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
  return (hi + 0.05) / (lo + 0.05);
}
const pairs = [
  ['#ffffff', '#0f766e', 'light বোতাম'],
  ['#042f2e', '#2dd4bf', 'dark বোতাম'],
  ['#ffffff', '#2dd4bf', 'ভুল: dark-এ সাদা লেখা'],
  ['#9ca3af', '#ffffff', 'সাদা পটে হালকা ধূসর'],
  ['#0f766e', '#111827', 'light-এর রং dark পটে'],
];
for (const [fg, bg, label] of pairs) {
  const r = contrast(fg, bg);
  const verdict = r >= 4.5 ? 'AA pass' : r >= 3 ? 'large only' : 'fail';
  console.log(r.toFixed(2), verdict, '-', label);
}

আউটপুট: 5.47 AA pass (light বোতাম), 7.77 AA pass (dark বোতাম), 1.86 fail (dark-এ সাদা লেখা), 2.54 fail (হালকা ধূসর), 3.24 large only (light-এর রং dark পটে)। তৃতীয় সারিটিই কারণ, dark থিমে --on-action সাদা রাখা যায়নি। Token জোড়ায় না ভাবলে এই ভুল চোখে পড়ে পরে। চতুর্থটি খুব প্রচলিত placeholder-ধাঁচের ধূসর, বাংলার সরু দাগে আরও কষ্ট দেয়। WCAG ২.২-এ 0.04045 মান ব্যবহার হয়, ২০২১ সালের আগের লেখায় 0.03928 পাবেন, এই রংগুলোর ফলে পার্থক্য পড়ে না।

১০ · AI-এর যুগে — Tailwind-এর পুরোনো স্মৃতি আর সুন্দর কিন্তু ভুল UI

Styling-এ AI খুব দ্রুত, আর ফলও দেখতে সুন্দর। বিপদ সেখানেই: চোখে ঠিক লাগে, কিন্তু কী ভেঙেছে তা পর্দায় দেখা যায় না। আমরা AI-এর একটি প্রচলিত ভুল সত্যিই চালিয়ে দেখলাম। v4 project-এ v3-এর দুটি নির্দেশ:

$ npx --no-install tailwindcss init
npm error could not determine executable to run

# index.css-এ @tailwind base; @tailwind components; @tailwind utilities;
$ npx vite build
dist/assets/index-BPih_y-5.css    3.93 kB │ gzip:  0.89 kB
✓ built
# CSS-এ .p-4 নেই, .bg-action নেই। .grid আর .mx-auto আছে।

দ্বিতীয়টি ভয়ংকর। Build সফল, error নেই, CSS ১১.১১ থেকে ৩.৯৩ kB-এ নেমেছে। Theme না আসায় theme-নির্ভর class হারিয়ে গেছে। AI-কে “ঠিক আছে তো?” জিজ্ঞেস করলে সে হয়তো “হ্যাঁ” বলবে। ধরা পড়ে শুধু চোখে দেখে আর CSS-এর আকার মেপে।

AI যা লেখেসমস্যাযাচাই
@tailwind base;, tailwindcss init, darkMode: 'class'v3-এর পথ, v4-এ নীরবে ভাঙেনিজের version-এর docs, package.json
`bg-${color}-600`CSS তৈরিই হয় নাbuild-এর CSS-এ class খুঁজুন
outline-none, <div onClick>Keyboard-এ ব্যবহার করা যায় নামাউস ছেড়ে শুধু Tab দিয়ে চালান
নকল prop বা প্যাকেজ, MUI আর Tailwind একসাথেকাজ করে না, অথবা bundle ফুলে যায়npm view, install-এর পরের diff
text-gray-400, কাঁচা hexContrast কম, token ভাঙেContrast মাপা, hex খোঁজা
“CSS-in-JS Server Component-এ চলে না”আধা-সত্য, library আর version-নির্ভরনিজে next build চালান

AI-কে কাজ দেওয়ার সময় version, token আর সীমা প্রথমেই বলে দিন, আর কী যাচাই করে দেখাবে তাও চান:

আমার project: React 19, Vite 8, Tailwind CSS v4.3 (tailwind.config.js নেই; theme আছে index.css-এর @theme-এ)। ব্যবহার করা যাবে শুধু এই token: bg-surface, text-ink, bg-action, text-on-action, bg-action-hover, rounded-card, font-bangla, leading-bn. একটি ProductBadge component বানাও, variant: stock, low, out. নিয়ম: ১. Class নাম string জোড়া দিয়ে বানাবে না; পুরো নাম একটি variant ম্যাপে রাখবে। ২. নতুন npm প্যাকেজ যোগ করবে না, শুধু clsx. ৩. কাঁচা hex রং লিখবে না। ৪. লেখা বাংলায়; line height ছোট করবে না, letter spacing দেবে না। শেষে বলো প্রতিটি variant-এর লেখা আর পটের রং জোড়া কী, যাতে আমি contrast নিজে মাপতে পারি।

AI-এর লেখা UI হাতে পেলে — যাচাইয়ের চেকলিস্ট

পরীক্ষা ও ইন্টারভিউতে: CSS Modules কী করে? Build-এর সময় class নামকে ফাইলভিত্তিক অনন্য নাম দেয়, ফলে scope হয়। Tailwind-এর CSS ছোট থাকে কেন? ফাইল স্ক্যান করে শুধু ব্যবহৃত utility বানায়, একই class-এর নিয়ম একবারই থাকে। bg-${color}-600 কেন চলে না? স্ক্যান সাধারণ লেখায় হয়, চলার সময়ের string সে দেখে না। Headless component কী? আচরণ ও accessibility দেয়, চেহারা নয়। WCAG AA contrast কত? সাধারণ লেখা 4.5:1, বড় লেখা 3:1.

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১Tailwind কি আসলে inline style-এর ফিরে আসা? “Separation of concerns” কি তাহলে ভুল ছিল?

দেখতে তাই লাগে, আর এই বিতর্কই Tailwind-এর শুরু থেকে চলছে। পুরোনো পরামর্শ ছিল HTML-এ অর্থবোধক নাম, CSS-এ চেহারা। class="product-card" ভালো, class="p-4" খারাপ। ছোট atomic class-এর ধারণা নতুন নয়, ২০১৩ সালের দিকে Thierry Koblentz-এর “Challenging CSS Best Practices” লেখা এই ধারার পরিচিত উদাহরণ। ২০১৭ সালে Tailwind-এর স্রষ্টা Adam Wathan “CSS Utility Classes and Separation of Concerns” প্রবন্ধে একটি অন্য যুক্তি দেন: .product-card-এর CSS আর markup আসলে আলাদা নয়। Markup বদলালে CSS বদলাতে হয়, তাই ফাইল আলাদা হলেও নির্ভরতা রয়ে যায়। তিনি প্রশ্নটি ঘুরিয়ে দেন, কে কার ওপর নির্ভর করবে।

Inline style থেকে পার্থক্য তিনটি বাস্তব জায়গায়। এক, style attribute-এ :hover, media query, :focus-visible লেখা যায় না, utility-তে যায়। দুই, inline style যেকোনো মান নেয়, তাই ১৩px আর ১৪px মিশে যায়। Utility বাঁধা মাপ থেকে আসে, যা একধরনের design system. তিন, inline style প্রতিটি element-এ পুরো লেখা বহন করে, utility-র CSS একবারই থাকে, আমাদের ১০০ component-এর run যা দেখিয়েছে।

React যুগে পুনরাবৃত্তির একক component, তাই একটি ProductCard-এর ভেতরে class থাকলে ক্ষতি কম। আবার সমালোচনাও সত্য: markup ভারী হয়, আর CSS জ্ঞান না থাকলে শুধু class মুখস্থ হয়। সাধারণ HTML পাতা বা CMS-এর লেখায় এটি ততটা মানায় না, যেখানে পুনরাবৃত্তি component-এ বাঁধা নেই।

মূল উপলব্ধি: Concern ভাগ হয় ফাইলের ধরন দিয়ে নয়, কাজের সীমা দিয়ে। Component-এর সীমাই যদি style-এর সীমা হয়, তবে একই ফাইলে লেখা নীতির লঙ্ঘন নয়, শুধু একটি আলাদা লেনদেন।

প্র ০২CSS-in-JS এত জনপ্রিয় হয়ে আবার কেন পিছু হটল? এই গল্প থেকে প্রযুক্তি বাছাই নিয়ে কী শেখা যায়?

২০১৪ সালে Christopher Chedeau-র “CSS in JS” বক্তৃতায় বড় codebase-এ CSS-এর কিছু সমস্যা তালিকা করা হয়: global নাম, dependency, dead code, ক্রম নিয়ে ভরসা ইত্যাদি। সমাধান ছিল style-কে component-এর ভেতরে আনা। styled-components (২০১৬ সালের দিকে) আর Emotion এই পথকে জনপ্রিয় করে। সেই সময়ের জন্য যুক্তি শক্ত ছিল: CSS variable তখনো সবার ব্যবহারে আসেনি, scope-এর সহজ উপায় কম ছিল।

তারপর পৃথিবী বদলাল। ২০২২ সালে Emotion-এর একজন maintainer Sam Magura “Why We're Breaking Up with CSS-in-JS” প্রবন্ধে নিজের অ্যাপে render খরচ মেপে দেখান, আর runtime পথ ছাড়ার কথা বলেন। Concurrent rendering-এ render-এর মাঝখানে style ঢোকানো কঠিন হয়। React দল এর জন্য useInsertionEffect আনে, তবে সেটি library-লেখকদের জন্য। Server Component আসায় Context-নির্ভরতা নতুন বাধা হয়। আমাদের run-এ Emotion-এর পাতা createContext-এ ভেঙেছে। ২০২৫ সালে styled-components maintenance mode-এর ঘোষণা দেয়।

কিন্তু “CSS-in-JS মৃত” বলাও ভুল। আমাদের run-এ styled-components ৬.৬ Server Component-এ build হয়েছে। MUI-এর মতো বড় library-র অনেক ব্যবহারকারী আছেন। ভালো ধারণাগুলো বেঁচে গেছে: style component-এর পাশে, type-সহ token, পুরোনো style স্বয়ংক্রিয়ভাবে মুছে যাওয়া। এগুলোই এখন Tailwind, CSS Modules আর zero-runtime library-তে আছে। শুধু কাজটি সরে গেছে চলার সময় থেকে build-এর সময়ে।

আরেকটি শিক্ষা, একটি টুল যে সমস্যার সমাধান করে, native platform পরে সেটি শিখে ফেলতে পারে। CSS variable, @layer, nesting, :where() এখন browser-এই আছে।

মূল উপলব্ধি: প্রতিটি জনপ্রিয় টুল কোনো নির্দিষ্ট সময়ের সীমাবদ্ধতার উত্তর। সীমাবদ্ধতা বদলালে উত্তরও পুরোনো হয়। তাই “কে জনপ্রিয়” নয়, জিজ্ঞেস করুন “এই টুল কোন সমস্যা মেটায়, আর সেটি কি এখনো আমার সমস্যা?”

প্র ০৩shadcn/ui-এর মতো কোড কপি করা কি npm dependency-র চেয়ে ভালো? Library-র আপডেট হারালে কী হয়?

সফটওয়্যারে সাধারণ শিক্ষা ছিল “কপি-পেস্ট নয়, dependency”. shadcn/ui এটিকে উল্টে দেয়। তার docs বলে এটি component library নয়, বরং নিজের library বানানোর একটি পথ। আমাদের run-এ button.tsx (৫৭ লাইন) আর dialog.tsx (১৫৮ লাইন) সরাসরি src-এ এসেছে।

লাভ: কোড আপনার। ডিজাইনারের চাওয়া একটি অদ্ভুত variant যোগ করতে override-এর লড়াই নেই, শুধু ফাইল বদলান। ব্যবহার না করা component bundle-এ আসে না, কারণ সেটি project-এই নেই। AI-ও সহজে পড়তে পারে, কারণ পুরো কোড সামনে।

দাম: Dependency-র একটি নীরব সুবিধা হলো আপডেট। Library-তে bug বা accessibility-র সমস্যা ঠিক হলে npm update-এই পৌঁছায়। কপি করা কোডে তা নিজে আসে না, আপনাকে তুলনা করে মেলাতে হয়, আর আপনি বদলে থাকলে কাজটি কঠিন। তবে পুরোটাই কপি নয়। আচরণের ভারী কাজ থাকে npm প্যাকেজে, Base UI বা Radix-এ, সেগুলো আপডেট পায়। কপি হয় মূলত চেহারার পাতলা স্তর। আমাদের run-এ @base-ui/react, lucide-react সহ কয়েকটি dependency-ও এসেছে। অর্থাৎ “dependency শূন্য” ধারণাটি ভুল।

একটি ভালো পরীক্ষা: এই কোডের মালিক কে হবে? দলে কেউ ১৫৮ লাইনের Dialog পড়ে বুঝতে না পারলে “কোড আপনার” কথাটি সুবিধা নয়, ঝুঁকি। কোম্পানির ভেতরের বড় UI kit হলে একটি নিজের npm প্যাকেজ বেশি মানায়, যাতে দশটি অ্যাপে একই fix পৌঁছায়। একক অ্যাপে কপি চলে।

মূল উপলব্ধি: Dependency মানে অন্যের রক্ষণাবেক্ষণ কিনে স্বাধীনতা দেওয়া। কপি মানে স্বাধীনতা নিয়ে রক্ষণাবেক্ষণের দায় নেওয়া। সঠিক উত্তর নির্ভর করে আপনি কোন দায়টি নিতে পারবেন।

প্র ০৪Primitive আর semantic token আলাদা কেন? ছোট দোকানের সাইটেও কি design system দরকার?

“Design token” কথাটি ২০১৪ সালের দিকে Salesforce-এর Lightning Design System-এর কাজে Jina Anne ও Jon Levine-এর নামের সাথে জড়িয়ে পরিচিত হয়। সমস্যা ছিল একই রং web, iOS, Android-এ আলাদা ফাইলে থাকা। সমাধান: সিদ্ধান্তগুলো এক জায়গায় ডেটা হিসেবে রাখা, সেখান থেকে প্রতিটি platform-এর কোড তৈরি। ২০২৫ সালের অক্টোবরে W3C-র Design Tokens Community Group-এর স্পেসিফিকেশনের প্রথম স্থির সংস্করণ এসেছে। এটি community group-এর কাজ, W3C-র মানদণ্ড (Recommendation) নয়।

দুটি স্তর কেন? --color-brand-600 বলে “কী রং”, --action বলে “কী কাজে”। Dark mode-এ পার্থক্যটি হাতেনাতে ধরা পড়ে। bg-brand-600 সরাসরি লিখলে dark-এ প্রতিটি জায়গায় dark: লিখতে হতো। bg-action লিখলে শুধু একটি variable বদলায়। Rebrand-এও তাই। নতুন রং এলে primitive বদলায়, অথবা semantic-টি অন্য primitive-কে দেখায়, component স্পর্শ করতে হয় না।

আবার বেশি স্তরও বিপদ। কিছু বড় system-এ তৃতীয় স্তর থাকে, component token, যেমন --button-primary-bg. ১০ পাতার সাইটে এই স্তর শুধু খোঁজার ঝামেলা বাড়ায়। নাম নিয়ে দীর্ঘ সভা, যা কেউ ব্যবহার করেন না এমন Storybook, এগুলো নকল কাজের ফাঁদ।

বাজারঘরের মতো একজন-দুজনের দলের জন্য যা যথেষ্ট, সেটি আমরা লিখেছি: এক ফাইলে কয়েকটি token, একটি Button, একটি ProductCard, তিনটি নিয়ম। যখন একই জিনিস তৃতীয়বার বানাতে হয়, তখন সেটি system-এ তুলুন। দ্বিতীয় অ্যাপ বা বড় দল এলে স্তর বাড়ান। আগেভাগে বড় system বানালে ভুল abstraction-এ আটকে যাওয়ার ঝুঁকি থাকে, যা ঠিক করা নতুন করে লেখার চেয়েও কঠিন হতে পারে।

মূল উপলব্ধি: Design system একটি পণ্য নয়, ধারাবাহিকতার খরচ কমানোর উপায়। যতটুকু ধারাবাহিকতা এখনই লাগে, ঠিক ততটুকু system গড়ুন।

প্র ০৫Contrast সূত্র পাস করলেই কি লেখা পড়া সহজ? বাংলার জন্য 4.5:1 কি যথেষ্ট?

Contrast সূত্র একটি মাপকাঠি, পড়ার সহজতার পূর্ণ মাপ নয়। WCAG ২.০ ২০০৮ সালে এই সূত্র আনে, পরের সংস্করণগুলো একই ধারায় চলছে। 4.5:1 সংখ্যাটির পেছনের যুক্তি W3C-র Understanding নথিতে আছে: সাধারণ দৃষ্টির জন্য প্রচলিত 3:1-এর সাথে দৃষ্টিশক্তি কম এমন মানুষের জন্য বাড়তি ছাড়। অর্থাৎ এটি মানুষের কথা ভেবে বানানো একটি বাস্তব সমঝোতা, প্রকৃতির নিয়ম নয়।

দুর্বলতাও জানা। সূত্রটি লেখার ওজন আর আকার সরাসরি ধরে না, শুধু “বড় লেখা” সীমা আছে। অনেক ডিজাইনার দেখেছেন, গাঢ় পটে হালকা লেখায় এটি মাঝে মাঝে খুব কড়া বা খুব নরম ফল দেয়, কারণ সূত্র উল্টালেও ফল একই, যদিও চোখের অনুভূতি এক নয়। তাই WCAG ৩-এর খসড়া কাজের আলোচনায় APCA নামে একটি অন্য পদ্ধতি নিয়ে কথা হয়েছে। সেটি এখনো নিয়ম হিসেবে ধরবেন না। আজকের চুক্তি বা পরীক্ষায় WCAG ২.x-এর সংখ্যাই চলে।

বাংলার ক্ষেত্রে একটি বাড়তি ভাবনা। সূত্রটি অক্ষরের আকার জানে না। বাংলার মাত্রা, রেফ, য-ফলা, হসন্ত খুব সরু দাগে তৈরি, ছোট আকারে হালকা রঙে এগুলো হারিয়ে যেতে পারে, “৩” আর “ত”-এর মতো কাছাকাছি আকার গুলিয়ে যেতে পারে। বাংলার জন্য আলাদা কোনো সরকারি সীমা আমাদের জানা নেই। তাই ন্যূনতম সীমার ওপরে থাকার চেষ্টা করুন, আর আসল ফোনে, রোদে, কম দামি পর্দায় দেখে নিন। আমাদের light বোতামের 5.47 আর dark বোতামের 7.77 সেই হিসাবে ভালো অবস্থান।

Contrast একা যথেষ্টও নয়। শুধু রং দিয়ে অবস্থা বোঝাবেন না, যেমন “লাল মানে স্টক শেষ”। লেখা বা icon-ও দিন।

মূল উপলব্ধি: মান পূরণ হলো সর্বনিম্ন সীমা, লক্ষ্য নয়। সূত্র যন্ত্রে মাপুন, আর শেষ বিচার মানুষের চোখে, আসল ভাষায়, আসল যন্ত্রে করুন।

অনুশীলন

হাতে হিসাব, নিজের project-এ চালানো, নকশা বিচার আর ফল আগেভাগে বলা।

  1. হাতে contrast: সাদা পটে #6b7280 লেখার contrast বের করুন। এর L = 0.1672 ধরে নিন। সাধারণ লেখায় AA পাশ করে কি? সাদা পটে #9ca3af-এর L = 0.3636 হলে কত?

    সাদার L = 1. প্রথমটি: (1 + 0.05) ÷ (0.1672 + 0.05) = 1.05 ÷ 0.2172 ≈ 4.83. 4.5 পার, পাশ। দ্বিতীয়টি: 1.05 ÷ 0.4136 ≈ 2.54. 3-এরও নিচে, বড় লেখাতেও ফেল। তাই হালকা ধূসর দেখতে সুন্দর হলেও সাধারণ লেখায় চলে না। পাতার calculator-এ এই দুটি রং বসিয়ে মিলিয়ে নিন।

  2. নিজের project-এ মাপুন: Vite-এ Tailwind বসিয়ে npm run build চালান। CSS-এর আকার লিখে রাখুন। তারপর একটি নতুন utility যোগ করুন, আর একই class-এর দশটি কার্ড যোগ করুন, প্রতিবার build দিন। শেষে bg-${c}-500 লিখে CSS ফাইলে grep করুন।

    আমাদের run-এ নতুন utility underline-এ CSS ১১.১১ থেকে ১১.১৫ kB হয়েছিল। ১০০ component-এ CSS ফাইলের নাম ও আকার অপরিবর্তিত ছিল। আপনার সংখ্যা আলাদা হবে, ধরন একই থাকার কথা। grep -c "bg-green-500" dist/assets/*.css dynamic class-এ 0 দেখাবে, আর পুরো নাম লিখলে 1. Windows PowerShell-এ Select-String ব্যবহার করতে পারেন।

  3. নকশা বিচার: একজন লিখেছেন: <div onClick={buy} className={`p-2 bg-${color}-500 text-gray-300 outline-none`} style={{letterSpacing:'2px'}}>কিনুন</div>. বাজারঘরে এই কোডে অন্তত পাঁচটি সমস্যা খুঁজুন, আর ভালো সংস্করণ লিখুন।
    • div keyboard-এ পৌঁছায় না, screen reader বোতাম বলে চেনে না। আসল button লাগবে।
    • bg-${color}-500-এর CSS তৈরি হবে না।
    • outline-none focus-এর দাগ মুছে দিচ্ছে, বদলি কিছু নেই।
    • text-gray-300 হালকা, contrast সম্ভবত কম। Token নেই।
    • letterSpacing বাংলার মাত্রা ভেঙে দেখায়।
    • Inline style আর একক class মিলে design system-এর বাইরে চলে গেছে।

    ভালো সংস্করণ: <Button variant="primary" onClick={buy}>কিনুন</Button>. বাকি সব কাজ component আর token-এর ভেতরে। কোন রং চাই, তা variant দিয়ে বলুন, রঙের নাম দিয়ে নয়।

  4. ফল আগে বলুন: (ক) clsx('a', false && 'b', { c: true, d: 0 }, ['e'], null) কী ফেরত দেবে? (খ) Tailwind-এর ডিফল্ট spacing-এ, ডিফল্ট px-4 থাকা বোতামে clsx দিয়ে পরে px-2 জুড়লে padding-left কত হবে, 8px না 16px?

    (ক) সত্যিকারের run-এ ফল "a c e". false, null বাদ, object-এ d: 0 মিথ্যা বলে বাদ, array খুলে e এলো।

    (খ) 16px. দুটি class-ই আছে, আর দুটির specificity সমান। জেতে CSS ফাইলে পরে থাকা .px-4. Class লেখার ক্রম গুরুত্ব পায় না। Edge-এ মেপেও 16px পাওয়া গেছে। twMerge ব্যবহার করলে px-4 বাদ পড়ত, তখন 8px.

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

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

ক্রমTailwind কীভাবে একটি class থেকে CSS বানায়, ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Project-এর ফাইল সাধারণ লেখা হিসেবে পড়া
  2. Class-এর মতো দেখতে টুকরো তুলে নেওয়া
  3. চেনা utility নয় এমন টুকরো বাদ দেওয়া
  4. Utility থেকে CSS নিয়ম আর variant-এর মোড়ক তৈরি
  5. Bundler-এর মাধ্যমে একটি CSS ফাইল হিসেবে বের হওয়া

দুটি আলাদা ফাইলে .card class আছে, দুটিই CSS Module. Build-এর পর কী হবে?

  • দুটি নিয়ম মিশে গিয়ে এক হবে
  • দুটি আলাদা অনন্য class নাম তৈরি হবে
  • Build error হবে
  • Browser চলার সময়ে একটি নিয়ম বেছে নেবে

Tailwind v4 project-এ className={`bg-${color}-600`} লিখলে রং আসছে না। মূল কারণ কী?

  • React template string নেয় না
  • Tailwind-এ ৬০০ মাত্রার রং নেই
  • স্ক্যানার পুরো নামটি ফাইলের লেখায় পায়নি, তাই CSS তৈরি হয়নি
  • Dark variant চালু করা হয়নি

Headless component library সবচেয়ে ভালোভাবে কোন কাজের বর্ণনা দেয়?

  • আচরণ ও accessibility দেয়, চেহারা নিজে বানাতে হয়
  • চেহারাসহ তৈরি component দেয়, শুধু theme বদলাতে হয়
  • Browser ছাড়া component test চালায়
  • শুধু server-এ চলে এমন component দেয়

লেখার L = 0.1418 আর পটের L = 1. Contrast ratio প্রায় কত, আর সাধারণ লেখায় WCAG AA পাশ করে কি?

  • 7.05, পাশ
  • 3.00, ফেল
  • 5.47, ফেল
  • 5.47, পাশ

একটি বোতামে className="px-4 px-2" আছে, দুটিই Tailwind class. কোনটি জিতবে তা কী ঠিক করে?

  • Class attribute-এ যেটি শেষে লেখা
  • তৈরি CSS ফাইলে যে নিয়মটি পরে আছে
  • যে class-এর মান বড়, সেটি সবসময়
  • React একটি class মুছে ফেলে

Theme বদলাতে <html>-এর একটি attribute বদলে CSS variable-এর মান পাল্টানো হলো। React component-গুলোর কী হবে?

  • সব component আবার render হবে
  • পাতায় আবার hydration হবে
  • কোনো re-render ছাড়াই browser নতুন রং দেখাবে
  • পুরো পাতা reload না করলে রং বদলাবে না

শব্দকোষ

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

মনে রাখুন

  1. CSS-এর মূল ঝামেলা global নাম আর specificity. CSS Modules নামে hash দেয়, Tailwind নতুন নামের দরকারই কমায়।
  2. Tailwind ফাইল সাধারণ লেখা হিসেবে স্ক্যান করে। Class-এর পুরো নাম লিখুন, জোড়া দিয়ে বানাবেন না।
  3. পুনরাবৃত্তির ওষুধ component: variant ম্যাপ, clsx, প্রয়োজনে tailwind-merge.
  4. Library মানে লেনদেন: headless, styled, কোড কপি। Runtime CSS-in-JS আর Server Component-এর সম্পর্ক version-এ মিলিয়ে দেখুন।
  5. Token রং ও মাপ এক জায়গায় রাখে। বাংলায় line height ১.৬–১.৮, focus-এর দাগ রাখুন, contrast 4.5:1 মাপুন।

পরের পর্বে: পর্ব ২৩-এ দেখবেন React performance: memo, code splitting, Suspense আর error boundary. আজ যে JS-এর আকার মাপলেন, সেটি ছোট করে ভাগে ভাগে পাঠানোর পথ।

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

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