Tailwind CSS چیست؟ آموزش کامل تیلویند با React و پروژه هوش مصنوعی

در این آموزش Tailwind CSS را از صفر یاد می‌گیرید؛ از نصب با Vite و Utility Classها تا طراحی Responsive، Dark Mode و Theme اختصاصی. در پایان یک اپلیکیشن هوش مصنوعی واقعی با React، Tailwind و API درواره می‌سازیم.

Share
Tailwind CSS چیست؟ آموزش کامل تیلویند با React و پروژه هوش مصنوعی


Tailwind CSS یکی از ابزارهای محبوب طراحی رابط کاربری وب است که به‌جای ارائه Componentهای آماده، مجموعه‌ای از Utility Classهای کوچک و قابل‌ترکیب در اختیار توسعه‌دهنده قرار می‌دهد.

در CSS سنتی معمولاً برای هر بخش رابط یک Class اختصاصی تعریف می‌کنیم:

.send-button {
  padding: 12px 20px;
  color: white;
  background-color: #6d4aff;
  border-radius: 12px;
}

در Tailwind همان ظاهر با ترکیب Utility Classها مستقیماً روی عنصر ساخته می‌شود:

<button
  className="
    rounded-xl
    bg-violet-600
    px-5
    py-3
    text-white
    hover:bg-violet-700
  "
>
  ارسال پیام
</button>

این رویکرد در ابتدا ممکن است شلوغ به نظر برسد، اما در پروژه‌های Component-based مانند React، Vue و Next.js می‌تواند سرعت توسعه و یکپارچگی طراحی را افزایش دهد.

در این مقاله Tailwind را فقط در حد معرفی چند Class بررسی نمی‌کنیم. ابتدا منطق Utility-first، نصب جدید با Vite، Responsive Design، Dark Mode، Theme Variables و الگوهای نگهداری کد را یاد می‌گیریم. سپس یک اپلیکیشن هوش مصنوعی فارسی با React، Tailwind CSS و API درواره می‌سازیم.

Tailwind CSS چیست؟

Tailwind CSS یک فریم‌ورک CSS مبتنی بر Utility Class است. هر Utility معمولاً یک یا چند ویژگی محدود CSS را اعمال می‌کند.

مثال:

<div class="p-6">
  محتوا
</div>

Class برابر با p-6 Padding را تنظیم می‌کند.

<div class="rounded-2xl">
  محتوا
</div>

Class برابر با rounded-2xl گوشه‌های عنصر را گرد می‌کند.

<div class="bg-white text-slate-900">
  محتوا
</div>

این Classها رنگ پس‌زمینه و متن را مشخص می‌کنند.

طبق مستندات رسمی Tailwind CSS، طراحی رابط با ترکیب Utility Classهای محدود و هدفمند انجام می‌شود و حالت‌هایی مانند Hover، Focus و Responsive نیز از طریق Variantها قابل‌تعریف هستند.

Utility-first CSS چیست؟

در روش سنتی ابتدا نامی برای Component انتخاب می‌کنیم و سپس قوانین CSS را در فایل جداگانه می‌نویسیم:

<article class="model-card">
  ...
</article>
.model-card {
  padding: 24px;
  background-color: white;
  border: 1px solid #e2e8f0;
  border-radius: 16px;
}

در روش Utility-first، ویژگی‌ها مستقیماً ترکیب می‌شوند:

<article
  class="
    rounded-2xl
    border
    border-slate-200
    bg-white
    p-6
  "
>
  ...
</article>

مزیت اصلی این است که هنگام خواندن Markup می‌توانید بخش زیادی از ظاهر عنصر را مشاهده کنید و برای هر تغییر کوچک مجبور نیستید میان فایل JSX و CSS جابه‌جا شوید.

آیا Tailwind جای CSS را می‌گیرد؟

خیر. Tailwind جای CSS را حذف نمی‌کند. Utility Classهای Tailwind در نهایت به CSS تبدیل می‌شوند.

برای استفاده حرفه‌ای از Tailwind همچنان باید مفاهیم زیر را بدانید:

  • Cascade
  • Specificity
  • Box Model
  • Flexbox
  • CSS Grid
  • Position
  • Overflow
  • Responsive Design
  • Typography
  • رنگ و Contrast
  • دسترس‌پذیری
  • RTL و Logical Properties

اگر ندانید flex-1، min-w-0، overflow-auto یا grid-cols-3 در CSS چه کاری انجام می‌دهند، Debug کردن رابط Tailwind دشوار خواهد شد.

مزایای Tailwind CSS

سرعت ساخت رابط

برای تغییر فاصله، رنگ، Border یا Layout لازم نیست برای هر مورد Class اختصاصی بسازید.

یکپارچگی طراحی

Tailwind از مقیاس‌های تعریف‌شده برای رنگ، فاصله، فونت، Radius و Breakpoint استفاده می‌کند. این موضوع احتمال استفاده از مقدارهای تصادفی را کاهش می‌دهد.

مناسب برای Component

در React، مجموعه Utilityها همراه Component نگهداری می‌شود:

function Badge({ children }) {
  return (
    <span
      className="
        inline-flex
        items-center
        rounded-full
        bg-violet-50
        px-3
        py-1
        text-xs
        font-semibold
        text-violet-700
      "
    >
      {children}
    </span>
  );
}

ساخت Variant

حالت‌های Hover، Focus، Disabled، Responsive و Dark Mode با Prefix ساخته می‌شوند:

<button
  class="
    bg-violet-600
    hover:bg-violet-700
    focus-visible:ring-4
    disabled:cursor-not-allowed
    disabled:opacity-50
    dark:bg-violet-500
  "
>
  ارسال
</button>

CSS خروجی هدفمند

Tailwind Classهایی را که در Source پیدا می‌کند وارد خروجی می‌کند. بنابراین لازم نیست کل مجموعه Utilityها به کاربر نهایی ارسال شود.

محدودیت‌های Tailwind CSS

طولانی‌شدن className

<button
  className="
    inline-flex
    min-h-11
    items-center
    justify-center
    gap-2
    rounded-xl
    bg-violet-600
    px-5
    py-2.5
    text-sm
    font-semibold
    text-white
    shadow-sm
    transition
    hover:bg-violet-700
    focus-visible:outline-none
    focus-visible:ring-4
    focus-visible:ring-violet-200
    disabled:cursor-not-allowed
    disabled:opacity-50
  "
>
  ارسال
</button>

در Componentهای پیچیده باید Classها را منظم و مسئولیت Component را محدود نگه دارید.

نیاز به دانش CSS

Tailwind مشکلات CSS را حذف نمی‌کند. فقط روش نوشتن آن را تغییر می‌دهد.

وابستگی به فرایند Build

برای تجربه کامل Tailwind معمولاً به ابزار Build نیاز دارید.

Classهای شرطی پیچیده

با افزایش Variantها، مدیریت رشته Class ممکن است دشوار شود. در این حالت می‌توان از تابع کمکی یا کتابخانه مدیریت Class استفاده کرد.

وابستگی ظاهری به Markup

تغییر طراحی معمولاً در فایل Component انجام می‌شود. برای بعضی تیم‌ها این مزیت و برای برخی دیگر محدودیت محسوب می‌شود.

تفاوت Tailwind و Bootstrap

ویژگیTailwind CSSBootstrap
رویکردUtility-firstComponent-first
ظاهر پیش‌فرضحداقلی و قابل‌شخصی‌سازیComponentهای آماده
سرعت Prototypeبالابسیار بالا
کنترل طراحیزیادنیازمند Override در طراحی خاص
JavaScript داخلینداردبرای بعضی Componentها دارد
مناسب Design System اختصاصیبسیار مناسبامکان‌پذیر با شخصی‌سازی
Classهای HTMLUtilityهای متعددClassهای Component

اگر می‌خواهید سریع یک رابط با Componentهای آماده بسازید، Bootstrap می‌تواند مناسب باشد. اگر Design System و ظاهر اختصاصی می‌خواهید، Tailwind انعطاف بیشتری ارائه می‌دهد.

تفاوت Tailwind و CSS Modules

CSS Modules و Tailwind رقیب مطلق یکدیگر نیستند.

CSS Module:

import styles from "./Button.module.css";

<button className={styles.primaryButton}>
  ارسال
</button>

Tailwind:

<button
  className="
    rounded-xl
    bg-violet-600
    px-5
    py-3
    text-white
  "
>
  ارسال
</button>

در یک پروژه می‌توان برای بیشتر طراحی از Tailwind و برای بعضی Styleهای پیچیده از CSS معمولی یا CSS Module استفاده کرد.

پیش‌نیازهای نصب Tailwind

برای پروژه این مقاله به موارد زیر نیاز دارید:

  • Node.js
  • npm
  • React
  • Vite
  • ویرایشگر کد
  • آشنایی مقدماتی با CSS و JavaScript

بررسی نصب Node.js و npm:

node --version
npm --version

ساخت پروژه React با Vite

دستور زیر را اجرا کنید:

npm create vite@latest tailwind-ai-dashboard -- --template react

وارد پوشه شوید:

cd tailwind-ai-dashboard

وابستگی‌های اولیه را نصب کنید:

npm install

نصب Tailwind CSS با Vite

براساس روش رسمی نصب Tailwind با Vite، بسته‌های زیر را نصب کنید:

npm install tailwindcss @tailwindcss/vite

فایل vite.config.js:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [
    react(),
    tailwindcss()
  ],

  server: {
    proxy: {
      "/api": {
        target: "http://localhost:3001",
        changeOrigin: true
      }
    }
  }
});

در فایل src/index.css:

@import "tailwindcss";

پروژه را اجرا کنید:

npm run dev

اکنون می‌توانید Utility Classهای Tailwind را در JSX استفاده کنید.

اولین Component با Tailwind

export function WelcomeCard() {
  return (
    <section
      className="
        mx-auto
        max-w-xl
        rounded-2xl
        border
        border-slate-200
        bg-white
        p-6
        shadow-sm
      "
    >
      <p
        className="
          text-sm
          font-semibold
          text-violet-600
        "
      >
        آموزش Tailwind
      </p>

      <h1
        className="
          mt-2
          text-2xl
          font-black
          text-slate-900
        "
      >
        اولین Component شما آماده است
      </h1>

      <p
        className="
          mt-3
          leading-8
          text-slate-600
        "
      >
        این کارت بدون تعریف Class اختصاصی ساخته شده است.
      </p>
    </section>
  );
}

ساختار نام Utilityها

بیشتر Utilityها ساختار قابل‌پیش‌بینی دارند:

ویژگی - مقدار

مثال:

p-4
mt-6
text-lg
bg-white
rounded-xl
grid-cols-3

گاهی Prefix حالت نیز اضافه می‌شود:

hover:bg-violet-700
md:grid-cols-2
dark:bg-slate-900
focus-visible:ring-4

ساختار مفهومی:

Variant : Utility

فاصله‌گذاری در Tailwind

Padding

<div class="p-4">Padding همه جهات</div>
<div class="px-6">Padding افقی</div>
<div class="py-3">Padding عمودی</div>
<div class="pt-4">Padding بالا</div>
<div class="pb-4">Padding پایین</div>

Margin

<div class="m-4">Margin همه جهات</div>
<div class="mx-auto">مرکز افقی</div>
<div class="mt-6">فاصله بالا</div>
<div class="mb-4">فاصله پایین</div>

Gap

برای فاصله میان فرزندان Flex و Grid:

<div class="flex gap-4">
  ...
</div>

در بسیاری از Layoutها استفاده از gap از Margin جداگانه برای هر فرزند ساده‌تر است.

اندازه در Tailwind

<div class="w-full">عرض کامل</div>
<div class="max-w-3xl">حداکثر عرض</div>
<div class="min-h-screen">حداقل ارتفاع صفحه</div>
<div class="size-12">عرض و ارتفاع برابر</div>

برای جلوگیری از خروج محتوای طولانی در Flex یا Grid:

<div class="min-w-0">
  ...
</div>

min-w-0 در رابط‌هایی مانند چت، جدول و داشبورد بسیار مهم است.

رنگ در Tailwind

<div class="bg-slate-950 text-white">
  محتوای تیره
</div>

Border:

<div class="border border-slate-200">
  ...
</div>

Opacity رنگ:

<div class="bg-violet-600/10">
  ...
</div>

رنگ متن:

<p class="text-slate-600">
  توضیحات
</p>

در پروژه حرفه‌ای بهتر است رنگ‌های برند را به Theme اضافه کنید و از رنگ‌های تصادفی متعدد اجتناب شود.

Typography

<h1 class="text-3xl font-black tracking-tight">
  عنوان اصلی
</h1>

<p class="text-base leading-8 text-slate-600">
  متن توضیحات
</p>

Classهای رایج:

Classکاربرد
text-smمتن کوچک
text-baseمتن پایه
text-xlمتن بزرگ
font-mediumوزن متوسط
font-boldمتن Bold
leading-8ارتفاع خط
tracking-tightفاصله کمتر حروف
text-centerتراز وسط
truncateکوتاه‌کردن متن تک‌خطی

برای متن فارسی، leading-7 یا leading-8 معمولاً خوانایی بهتری نسبت به Line Height بسیار فشرده دارد.

Border، Radius و Shadow

<div
  class="
    rounded-2xl
    border
    border-slate-200
    shadow-sm
  "
>
  ...
</div>

نمونه دکمه:

<button
  class="
    rounded-xl
    bg-violet-600
    px-4
    py-2
    text-white
    shadow-lg
    shadow-violet-600/20
  "
>
  ادامه
</button>

Shadow زیاد در تمام Componentها رابط را شلوغ می‌کند. بهتر است سلسله‌مراتب بصری مشخصی داشته باشید.

Flexbox در Tailwind

<div
  class="
    flex
    items-center
    justify-between
    gap-4
  "
>
  <span>عنوان</span>
  <button>عملیات</button>
</div>

Classهای پرکاربرد:

TailwindCSS تقریبی
flexdisplay: flex
flex-colflex-direction: column
items-centeralign-items: center
justify-betweenjustify-content: space-between
flex-1رشد عنصر
shrink-0جلوگیری از کوچک‌شدن
flex-wrapانتقال به خط بعد
gap-4فاصله میان آیتم‌ها

CSS Grid در Tailwind

<div
  class="
    grid
    grid-cols-1
    gap-5
    md:grid-cols-2
    xl:grid-cols-3
  "
>
  ...
</div>

برای Sidebar و محتوای اصلی:

<div
  class="
    grid
    min-h-screen
    grid-cols-1
    lg:grid-cols-[280px_minmax(0,1fr)]
  "
>
  ...
</div>

مقدار داخل [] یک Arbitrary Value است و برای Layout اختصاصی استفاده می‌شود.

Arbitrary Value چیست؟

اگر مقدار موردنیاز در مقیاس پیش‌فرض وجود ندارد، می‌توان از مقدار دلخواه استفاده کرد:

<div class="max-w-[920px]">
  ...
</div>

رنگ اختصاصی:

<div class="bg-[#6d4aff]">
  ...
</div>

Grid اختصاصی:

<div class="grid-cols-[260px_1fr]">
  ...
</div>

Arbitrary Value مفید است، اما استفاده زیاد از مقدارهای تصادفی می‌تواند Design System را از بین ببرد. مقدارهای پرتکرار را به Theme تبدیل کنید.

Responsive Design در Tailwind

Tailwind از رویکرد Mobile-first استفاده می‌کند. Utility بدون Prefix برای همه اندازه‌ها اعمال می‌شود و Prefixهایی مانند md: یا lg: از Breakpoint مشخص به بالا فعال می‌شوند.

<div
  class="
    grid
    grid-cols-1
    gap-4
    md:grid-cols-2
    xl:grid-cols-4
  "
>
  ...
</div>

یعنی:

  • موبایل: یک ستون
  • صفحه متوسط: دو ستون
  • صفحه بزرگ: چهار ستون

در مستندات Responsive Design تیلویند، Variantهای Breakpoint و رویکرد Mobile-first توضیح داده شده‌اند.

اشتباه رایج در Mobile-first

اگر می‌خواهید عنصر فقط در موبایل دیده شود:

<div class="block md:hidden">
  منوی موبایل
</div>

اگر می‌خواهید فقط از md به بالا دیده شود:

<div class="hidden md:block">
  منوی دسکتاپ
</div>

Hover، Focus و Disabled

<button
  class="
    rounded-xl
    bg-violet-600
    px-5
    py-3
    text-white
    transition
    hover:bg-violet-700
    focus-visible:outline-none
    focus-visible:ring-4
    focus-visible:ring-violet-200
    disabled:cursor-not-allowed
    disabled:opacity-50
  "
>
  ارسال
</button>

برای دسترس‌پذیری، Focus قابل‌مشاهده را حذف نکنید. Hover نیز نباید تنها راه انتقال اطلاعات ضروری باشد.

Group و Peer

Group

برای تغییر فرزند براساس وضعیت والد:

<a
  class="
    group
    block
    rounded-xl
    border
    border-slate-200
    p-4
    hover:border-violet-300
  "
>
  <h3
    class="
      font-bold
      text-slate-900
      group-hover:text-violet-700
    "
  >
    مدل هوش مصنوعی
  </h3>
</a>

Peer

برای تغییر عنصر براساس وضعیت همسایه:

<label>
  <input
    type="checkbox"
    class="peer sr-only"
  >

  <span
    class="
      text-slate-500
      peer-checked:text-violet-700
    "
  >
    فعال‌سازی
  </span>
</label>

RTL در Tailwind

جهت صفحه را در HTML مشخص کنید:

<html lang="fa" dir="rtl">

برای تراز متن:

<p class="text-right">
  متن فارسی
</p>

برای بخش‌های کد و Model ID:

<code
  dir="ltr"
  class="block text-left font-mono"
>
  YOUR_MODEL_ID
</code>

تا حد امکان Layout را با Flex، Grid و gap بسازید تا وابستگی مستقیم به چپ و راست کمتر شود.

در موارد لازم می‌توان از Utilityهای منطقی مانند ms-* و me-* استفاده کرد:

<span class="ms-2">
  متن
</span>

ms به ابتدای محور Inline و me به انتهای آن اشاره می‌کند و با RTL سازگارتر است.

Theme اختصاصی در Tailwind

در نسخه‌های جدید Tailwind می‌توان Theme Variableها را در CSS تعریف کرد.

فایل src/index.css:

@import "tailwindcss";

@theme {
  --color-darvareh-50: #f4f1ff;
  --color-darvareh-100: #ebe5ff;
  --color-darvareh-500: #7b5cff;
  --color-darvareh-600: #6d4aff;
  --color-darvareh-700: #5636db;

  --font-sans:
    Vazirmatn,
    Tahoma,
    Arial,
    sans-serif;

  --radius-card: 1.25rem;
}

سپس از Utilityهای مرتبط استفاده کنید:

<button
  class="
    rounded-card
    bg-darvareh-600
    text-white
    hover:bg-darvareh-700
  "
>
  شروع
</button>

Theme Variableها برای این موارد مناسب‌اند:

  • رنگ برند
  • فونت
  • Breakpoint
  • Radius
  • Shadow
  • Spacing
  • Animation

جزئیات بیشتر در مستندات Theme Variables تیلویند ارائه شده است.

Dark Mode در Tailwind

برای استفاده براساس تنظیم سیستم:

<div
  class="
    bg-white
    text-slate-900
    dark:bg-slate-950
    dark:text-slate-100
  "
>
  ...
</div>

برای کنترل دستی با Class، در CSS یک Variant تعریف کنید:

@import "tailwindcss";

@custom-variant dark (
  &:where(.dark, .dark *)
);

سپس Class برابر با dark را روی عنصر HTML قرار دهید:

document.documentElement.classList.add("dark");

حذف حالت تیره:

document.documentElement.classList.remove("dark");

تغییر حالت:

document.documentElement.classList.toggle("dark");

راهنمای کامل این روش در مستندات Dark Mode تیلویند موجود است.

ذخیره Theme انتخابی کاربر

function applyTheme(theme) {
  const isDark = theme === "dark";

  document.documentElement.classList.toggle(
    "dark",
    isDark
  );

  localStorage.setItem("theme", theme);
}

const savedTheme =
  localStorage.getItem("theme");

const systemPrefersDark =
  window.matchMedia(
    "(prefers-color-scheme: dark)"
  ).matches;

applyTheme(
  savedTheme ??
  (systemPrefersDark ? "dark" : "light")
);

Theme یک تنظیم عمومی است و ذخیره آن در Local Storage معمولاً اشکالی ندارد. اما API Key یا اطلاعات حساس را در Local Storage ذخیره نکنید.

پروژه عملی: داشبورد هوش مصنوعی با React و Tailwind

در این پروژه ابزاری برای پردازش متن می‌سازیم که کاربر می‌تواند:

  • متن فارسی وارد کند.
  • نوع عملیات را انتخاب کند.
  • از مدل بخواهد متن را خلاصه کند.
  • متن را بازنویسی کند.
  • نکات کلیدی را استخراج کند.
  • لحن متن را حرفه‌ای‌تر کند.
  • پاسخ را در رابط مشاهده کند.
  • درخواست را متوقف کند.
  • رابط را در موبایل استفاده کند.
  • Theme روشن یا تیره را انتخاب کند.

API Key فقط در Backend نگهداری می‌شود.

ساختار پروژه

tailwind-ai-dashboard/
├── backend/
│   ├── .env
│   ├── package.json
│   └── server.js
├── src/
│   ├── components/
│   │   ├── ActionSelector.jsx
│   │   ├── AppHeader.jsx
│   │   ├── ResultPanel.jsx
│   │   └── TextForm.jsx
│   ├── services/
│   │   └── textApi.js
│   ├── App.jsx
│   ├── index.css
│   └── main.jsx
├── .gitignore
├── index.html
├── package.json
└── vite.config.js

تنظیم CSS پروژه

فایل src/index.css:

@import "tailwindcss";

@custom-variant dark (
  &:where(.dark, .dark *)
);

@theme {
  --color-darvareh-50: #f4f1ff;
  --color-darvareh-100: #eae4ff;
  --color-darvareh-500: #7b5cff;
  --color-darvareh-600: #6d4aff;
  --color-darvareh-700: #5536dc;

  --font-sans:
    Vazirmatn,
    Tahoma,
    Arial,
    sans-serif;
}

html {
  min-width: 320px;
  min-height: 100%;
}

body {
  min-width: 320px;
  min-height: 100dvh;
  margin: 0;
}

button,
textarea {
  font: inherit;
}

ساخت Service درخواست

فایل src/services/textApi.js:

export async function processText({
  text,
  action,
  signal
}) {
  const response = await fetch("/api/process", {
    method: "POST",

    headers: {
      "Content-Type": "application/json"
    },

    body: JSON.stringify({
      text,
      action
    }),

    signal
  });

  const contentType =
    response.headers.get("content-type") || "";

  if (!contentType.includes("application/json")) {
    throw new Error(
      "پاسخ سرور در قالب مورد انتظار نیست."
    );
  }

  const data = await response.json();

  if (!response.ok) {
    throw new Error(
      data.error ||
      `خطای HTTP: ${response.status}`
    );
  }

  if (
    typeof data.result !== "string" ||
    !data.result.trim()
  ) {
    throw new Error(
      "نتیجه معتبری دریافت نشد."
    );
  }

  return data.result;
}

Component انتخاب عملیات

فایل src/components/ActionSelector.jsx:

const actions = [
  {
    id: "summarize",
    title: "خلاصه‌سازی",
    description: "تبدیل متن به خلاصه‌ای دقیق"
  },
  {
    id: "rewrite",
    title: "بازنویسی",
    description: "روان‌تر و حرفه‌ای‌تر کردن متن"
  },
  {
    id: "key_points",
    title: "نکات کلیدی",
    description: "استخراج مهم‌ترین نکات متن"
  },
  {
    id: "formal",
    title: "لحن رسمی",
    description: "تبدیل متن به نگارش رسمی"
  }
];

export function ActionSelector({
  value,
  onChange,
  disabled
}) {
  return (
    <fieldset disabled={disabled}>
      <legend
        className="
          mb-3
          text-sm
          font-bold
          text-slate-900
          dark:text-white
        "
      >
        نوع پردازش را انتخاب کنید
      </legend>

      <div
        className="
          grid
          grid-cols-1
          gap-3
          sm:grid-cols-2
          xl:grid-cols-4
        "
      >
        {actions.map((action) => {
          const isSelected =
            value === action.id;

          return (
            <label
              key={action.id}
              className={`
                cursor-pointer
                rounded-2xl
                border
                p-4
                transition
                ${
                  isSelected
                    ? `
                      border-darvareh-500
                      bg-darvareh-50
                      ring-4
                      ring-darvareh-500/10
                      dark:bg-darvareh-500/10
                    `
                    : `
                      border-slate-200
                      bg-white
                      hover:border-darvareh-300
                      dark:border-slate-700
                      dark:bg-slate-900
                    `
                }
              `}
            >
              <input
                className="sr-only"
                type="radio"
                name="action"
                value={action.id}
                checked={isSelected}
                onChange={(event) => {
                  onChange(event.target.value);
                }}
              />

              <span
                className="
                  block
                  font-bold
                  text-slate-900
                  dark:text-white
                "
              >
                {action.title}
              </span>

              <span
                className="
                  mt-1
                  block
                  text-xs
                  leading-6
                  text-slate-500
                  dark:text-slate-400
                "
              >
                {action.description}
              </span>
            </label>
          );
        })}
      </div>
    </fieldset>
  );
}

در این Component، Classهای محدود و مشخص به‌صورت شرطی انتخاب می‌شوند. همه Classها به شکل کامل در Source وجود دارند و ابزار Build می‌تواند آن‌ها را شناسایی کند.

چرا Class پویا را کامل می‌نویسیم؟

این الگو مناسب نیست:

const color = "violet";

<div className={`bg-${color}-600`}>
  ...
</div>

ابزار تولید CSS معمولاً Source را به‌صورت متن بررسی می‌کند و ممکن است Class ساخته‌شده در Runtime را پیدا نکند.

نسخه مناسب:

const colorClasses = {
  violet: "bg-violet-600",
  blue: "bg-blue-600",
  emerald: "bg-emerald-600"
};

<div className={colorClasses[color]}>
  ...
</div>

Classهای کامل و قابل‌شناسایی را در Source قرار دهید.

ساخت Header و Dark Mode

فایل src/components/AppHeader.jsx:

export function AppHeader({
  isDark,
  onToggleTheme
}) {
  return (
    <header
      className="
        flex
        flex-col
        gap-5
        border-b
        border-slate-200
        px-5
        py-5
        dark:border-slate-800
        sm:flex-row
        sm:items-center
        sm:justify-between
        lg:px-8
      "
    >
      <div>
        <p
          className="
            text-xs
            font-bold
            text-darvareh-600
            dark:text-darvareh-500
          "
        >
          پروژه React و Tailwind CSS
        </p>

        <h1
          className="
            mt-1
            text-xl
            font-black
            tracking-tight
            text-slate-950
            dark:text-white
            sm:text-2xl
          "
        >
          پردازش متن با هوش مصنوعی
        </h1>
      </div>

      <div className="flex items-center gap-3">
        <a
          className="
            rounded-xl
            border
            border-slate-200
            px-4
            py-2
            text-sm
            font-semibold
            text-slate-700
            transition
            hover:border-darvareh-300
            hover:text-darvareh-700
            focus-visible:outline-none
            focus-visible:ring-4
            focus-visible:ring-darvareh-500/15
            dark:border-slate-700
            dark:text-slate-200
          "
          href="https://darvareh.ir/models"
          target="_blank"
          rel="noreferrer"
        >
          مدل‌ها
        </a>

        <button
          className="
            inline-flex
            size-10
            items-center
            justify-center
            rounded-xl
            bg-slate-100
            text-lg
            text-slate-700
            transition
            hover:bg-slate-200
            focus-visible:outline-none
            focus-visible:ring-4
            focus-visible:ring-darvareh-500/15
            dark:bg-slate-800
            dark:text-slate-200
            dark:hover:bg-slate-700
          "
          type="button"
          onClick={onToggleTheme}
          aria-label={
            isDark
              ? "فعال‌کردن حالت روشن"
              : "فعال‌کردن حالت تیره"
          }
        >
          <span aria-hidden="true">
            {isDark ? "☀" : "☾"}
          </span>
        </button>
      </div>
    </header>
  );
}

ساخت فرم متن

فایل src/components/TextForm.jsx:

import {
  useState
} from "react";

import {
  ActionSelector
} from "./ActionSelector.jsx";

export function TextForm({
  isLoading,
  onSubmit,
  onCancel
}) {
  const [text, setText] = useState("");
  const [action, setAction] =
    useState("summarize");

  const [error, setError] = useState("");

  async function handleSubmit(event) {
    event.preventDefault();

    const normalizedText = text.trim();

    if (!normalizedText) {
      setError("لطفاً متن را وارد کنید.");
      return;
    }

    if (normalizedText.length > 12000) {
      setError(
        "متن نباید بیشتر از ۱۲۰۰۰ کاراکتر باشد."
      );

      return;
    }

    setError("");

    await onSubmit({
      text: normalizedText,
      action
    });
  }

  return (
    <form
      className="
        space-y-6
        rounded-3xl
        border
        border-slate-200
        bg-white
        p-5
        shadow-sm
        dark:border-slate-800
        dark:bg-slate-900
        lg:p-7
      "
      onSubmit={handleSubmit}
    >
      <ActionSelector
        value={action}
        onChange={setAction}
        disabled={isLoading}
      />

      <div>
        <div
          className="
            mb-2
            flex
            items-center
            justify-between
            gap-4
          "
        >
          <label
            className="
              text-sm
              font-bold
              text-slate-900
              dark:text-white
            "
            htmlFor="source-text"
          >
            متن ورودی
          </label>

          <span
            className="
              text-xs
              text-slate-500
              dark:text-slate-400
            "
          >
            {text.length.toLocaleString("fa-IR")}
            {" / "}
            ۱۲٬۰۰۰
          </span>
        </div>

        <textarea
          id="source-text"
          className="
            min-h-72
            w-full
            resize-y
            rounded-2xl
            border
            border-slate-200
            bg-slate-50
            px-4
            py-3
            leading-8
            text-slate-900
            outline-none
            transition
            placeholder:text-slate-400
            focus:border-darvareh-500
            focus:bg-white
            focus:ring-4
            focus:ring-darvareh-500/10
            disabled:cursor-not-allowed
            disabled:opacity-60
            dark:border-slate-700
            dark:bg-slate-950
            dark:text-white
            dark:focus:border-darvareh-500
          "
          value={text}
          maxLength="12000"
          disabled={isLoading}
          placeholder="متنی را که می‌خواهید پردازش شود وارد کنید..."
          onChange={(event) => {
            setText(event.target.value);

            if (error) {
              setError("");
            }
          }}
        />
      </div>

      {error && (
        <p
          className="
            rounded-xl
            border
            border-red-200
            bg-red-50
            px-4
            py-3
            text-sm
            text-red-700
            dark:border-red-900
            dark:bg-red-950/40
            dark:text-red-300
          "
          role="alert"
        >
          {error}
        </p>
      )}

      <div
        className="
          flex
          flex-col-reverse
          gap-3
          sm:flex-row
          sm:justify-end
        "
      >
        {isLoading && (
          <button
            className="
              min-h-11
              rounded-xl
              bg-slate-100
              px-5
              py-2.5
              text-sm
              font-bold
              text-slate-700
              transition
              hover:bg-slate-200
              focus-visible:outline-none
              focus-visible:ring-4
              focus-visible:ring-slate-200
              dark:bg-slate-800
              dark:text-slate-200
            "
            type="button"
            onClick={onCancel}
          >
            توقف پردازش
          </button>
        )}

        <button
          className="
            min-h-11
            rounded-xl
            bg-darvareh-600
            px-6
            py-2.5
            text-sm
            font-bold
            text-white
            shadow-lg
            shadow-darvareh-600/20
            transition
            hover:-translate-y-0.5
            hover:bg-darvareh-700
            focus-visible:outline-none
            focus-visible:ring-4
            focus-visible:ring-darvareh-500/20
            disabled:cursor-not-allowed
            disabled:opacity-50
          "
          type="submit"
          disabled={
            isLoading || !text.trim()
          }
        >
          {isLoading
            ? "در حال پردازش..."
            : "پردازش با هوش مصنوعی"}
        </button>
      </div>
    </form>
  );
}

ساخت پنل نتیجه

فایل src/components/ResultPanel.jsx:

import {
  useState
} from "react";

export function ResultPanel({
  result,
  error,
  isLoading
}) {
  const [copied, setCopied] = useState(false);

  async function handleCopy() {
    if (!result) {
      return;
    }

    await navigator.clipboard.writeText(result);

    setCopied(true);

    setTimeout(() => {
      setCopied(false);
    }, 1500);
  }

  return (
    <section
      className="
        flex
        min-h-[420px]
        min-w-0
        flex-col
        rounded-3xl
        border
        border-slate-200
        bg-white
        p-5
        shadow-sm
        dark:border-slate-800
        dark:bg-slate-900
        lg:p-7
      "
    >
      <div
        className="
          flex
          items-center
          justify-between
          gap-4
        "
      >
        <h2
          className="
            font-black
            text-slate-950
            dark:text-white
          "
        >
          نتیجه
        </h2>

        <button
          className="
            rounded-xl
            bg-slate-100
            px-3
            py-2
            text-xs
            font-bold
            text-slate-700
            hover:bg-slate-200
            disabled:cursor-not-allowed
            disabled:opacity-40
            dark:bg-slate-800
            dark:text-slate-200
          "
          type="button"
          disabled={!result || isLoading}
          onClick={handleCopy}
        >
          {copied ? "کپی شد" : "کپی نتیجه"}
        </button>
      </div>

      <div
        className="
          mt-5
          flex-1
          overflow-auto
          rounded-2xl
          bg-slate-50
          p-4
          dark:bg-slate-950
        "
      >
        {isLoading && (
          <div
            className="
              flex
              h-full
              min-h-64
              items-center
              justify-center
            "
          >
            <div className="text-center">
              <span
                className="
                  mx-auto
                  block
                  size-10
                  animate-spin
                  rounded-full
                  border-4
                  border-darvareh-100
                  border-t-darvareh-600
                "
                aria-hidden="true"
              ></span>

              <p
                className="
                  mt-4
                  text-sm
                  text-slate-500
                  dark:text-slate-400
                "
              >
                مدل در حال پردازش متن است...
              </p>
            </div>
          </div>
        )}

        {!isLoading && error && (
          <p
            className="
              rounded-xl
              border
              border-red-200
              bg-red-50
              p-4
              text-sm
              leading-7
              text-red-700
              dark:border-red-900
              dark:bg-red-950/40
              dark:text-red-300
            "
            role="alert"
          >
            {error}
          </p>
        )}

        {!isLoading && !error && result && (
          <p
            className="
              whitespace-pre-wrap
              break-words
              leading-8
              text-slate-700
              dark:text-slate-200
            "
          >
            {result}
          </p>
        )}

        {!isLoading && !error && !result && (
          <div
            className="
              flex
              h-full
              min-h-64
              items-center
              justify-center
              text-center
            "
          >
            <div>
              <div
                className="
                  mx-auto
                  grid
                  size-14
                  place-items-center
                  rounded-2xl
                  bg-darvareh-50
                  text-2xl
                  text-darvareh-600
                  dark:bg-darvareh-500/10
                "
                aria-hidden="true"
              >
                ✦
              </div>

              <p
                className="
                  mt-4
                  text-sm
                  leading-7
                  text-slate-500
                  dark:text-slate-400
                "
              >
                متن را وارد و نوع پردازش را انتخاب کنید.
              </p>
            </div>
          </div>
        )}
      </div>
    </section>
  );
}

ساخت App اصلی

فایل src/App.jsx:

import {
  useEffect,
  useRef,
  useState
} from "react";

import {
  AppHeader
} from "./components/AppHeader.jsx";

import {
  ResultPanel
} from "./components/ResultPanel.jsx";

import {
  TextForm
} from "./components/TextForm.jsx";

import {
  processText
} from "./services/textApi.js";

function getInitialTheme() {
  const savedTheme =
    localStorage.getItem("theme");

  if (savedTheme === "dark") {
    return true;
  }

  if (savedTheme === "light") {
    return false;
  }

  return window.matchMedia(
    "(prefers-color-scheme: dark)"
  ).matches;
}

export default function App() {
  const [isDark, setIsDark] =
    useState(getInitialTheme);

  const [result, setResult] = useState("");
  const [error, setError] = useState("");
  const [isLoading, setIsLoading] =
    useState(false);

  const controllerRef = useRef(null);

  useEffect(() => {
    document.documentElement.classList.toggle(
      "dark",
      isDark
    );

    localStorage.setItem(
      "theme",
      isDark ? "dark" : "light"
    );
  }, [isDark]);

  async function handleProcess({
    text,
    action
  }) {
    setIsLoading(true);
    setError("");
    setResult("");

    const controller =
      new AbortController();

    controllerRef.current = controller;

    try {
      const output = await processText({
        text,
        action,
        signal: controller.signal
      });

      setResult(output);
    } catch (requestError) {
      if (requestError.name === "AbortError") {
        setError("درخواست متوقف شد.");
        return;
      }

      setError(
        requestError.message ||
        "خطای غیرمنتظره رخ داد."
      );
    } finally {
      controllerRef.current = null;
      setIsLoading(false);
    }
  }

  function handleCancel() {
    controllerRef.current?.abort();
  }

  return (
    <div
      className="
        min-h-dvh
        bg-slate-100
        px-0
        py-0
        text-slate-900
        transition-colors
        dark:bg-slate-950
        dark:text-white
        sm:px-5
        sm:py-5
        lg:px-8
      "
    >
      <main
        className="
          mx-auto
          min-h-dvh
          max-w-7xl
          overflow-hidden
          bg-slate-50
          shadow-2xl
          shadow-slate-900/5
          dark:bg-slate-950
          sm:min-h-[calc(100dvh-2.5rem)]
          sm:rounded-3xl
          sm:border
          sm:border-slate-200
          dark:sm:border-slate-800
        "
      >
        <AppHeader
          isDark={isDark}
          onToggleTheme={() => {
            setIsDark((current) => !current);
          }}
        />

        <div
          className="
            grid
            grid-cols-1
            gap-5
            p-4
            lg:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]
            lg:p-8
          "
        >
          <TextForm
            isLoading={isLoading}
            onSubmit={handleProcess}
            onCancel={handleCancel}
          />

          <ResultPanel
            result={result}
            error={error}
            isLoading={isLoading}
          />
        </div>

        <p
          className="
            px-5
            pb-6
            text-center
            text-xs
            text-slate-500
            dark:text-slate-400
          "
        >
          خروجی هوش مصنوعی ممکن است نیازمند بررسی باشد.
        </p>
      </main>
    </div>
  );
}

ساخت Backend امن

پوشه Backend را ایجاد کنید:

mkdir backend
cd backend
npm init -y
npm install express dotenv

فایل backend/package.json:

{
  "name": "tailwind-ai-backend",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "start": "node server.js",
    "dev": "node --watch server.js"
  },
  "dependencies": {
    "dotenv": "^17.0.0",
    "express": "^5.0.0"
  }
}

نسخه واقعی وابستگی‌ها ممکن است هنگام نصب متفاوت باشد. نسخه ثبت‌شده توسط npm در package.json و package-lock.json را مبنا قرار دهید.

فایل backend/server.js:

import "dotenv/config";

import express from "express";

const app = express();

const port = Number(
  process.env.PORT || 3001
);

const apiKey =
  process.env.DARVAREH_API_KEY;

const modelId =
  process.env.DARVAREH_MODEL_ID;

const chatUrl =
  "https://api.darvareh.ir/v1/chat/completions";

const actionInstructions = {
  summarize:
    "متن را دقیق و فشرده خلاصه کن. نکات مهم حذف نشوند.",

  rewrite:
    "متن را روان، منسجم و حرفه‌ای بازنویسی کن. معنی اصلی تغییر نکند.",

  key_points:
    "مهم‌ترین نکات متن را به‌صورت فهرست روشن استخراج کن.",

  formal:
    "متن را با لحن رسمی و حرفه‌ای بازنویسی کن. اطلاعات اصلی حفظ شوند."
};

app.disable("x-powered-by");

app.use(
  express.json({
    limit: "64kb"
  })
);

function validateInput(text, action) {
  return (
    typeof text === "string" &&
    text.trim().length > 0 &&
    text.length <= 12000 &&
    Object.hasOwn(
      actionInstructions,
      action
    )
  );
}

app.post(
  "/api/process",
  async (request, response) => {
    if (!apiKey || !modelId) {
      return response.status(500).json({
        error:
          "تنظیمات API در سرور کامل نیست."
      });
    }

    const {
      text,
      action
    } = request.body ?? {};

    if (!validateInput(text, action)) {
      return response.status(400).json({
        error:
          "متن یا نوع عملیات معتبر نیست."
      });
    }

    const controller =
      new AbortController();

    const timeoutId = setTimeout(() => {
      controller.abort();
    }, 60000);

    try {
      const upstreamResponse = await fetch(
        chatUrl,
        {
          method: "POST",

          headers: {
            "Authorization":
              `Bearer ${apiKey}`,

            "Content-Type":
              "application/json"
          },

          body: JSON.stringify({
            model: modelId,

            messages: [
              {
                role: "system",

                content:
                  "شما یک دستیار حرفه‌ای پردازش متن فارسی هستید. " +
                  "اطلاعاتی به متن اضافه نکنید مگر اینکه کاربر صریحاً درخواست کند."
              },
              {
                role: "user",

                content:
                  `${actionInstructions[action]}\n\n` +
                  `متن ورودی:\n${text.trim()}`
              }
            ],

            temperature: 0.3,
            max_tokens: 1600
          }),

          signal: controller.signal
        }
      );

      const rawBody =
        await upstreamResponse.text();

      let data;

      try {
        data = JSON.parse(rawBody);
      } catch {
        return response.status(502).json({
          error:
            "پاسخ سرویس مدل قابل پردازش نبود."
        });
      }

      if (!upstreamResponse.ok) {
        console.error(
          "Model request failed:",
          upstreamResponse.status
        );

        return response.status(502).json({
          error:
            "سرویس مدل پاسخ موفقی ارسال نکرد."
        });
      }

      const result =
        data?.choices?.[0]?.message?.content;

      if (
        typeof result !== "string" ||
        !result.trim()
      ) {
        return response.status(502).json({
          error:
            "نتیجه معتبری از مدل دریافت نشد."
        });
      }

      return response.json({
        result
      });
    } catch (error) {
      if (error.name === "AbortError") {
        return response.status(504).json({
          error:
            "زمان انتظار برای پاسخ به پایان رسید."
        });
      }

      console.error(
        "Process request failed:",
        error.message
      );

      return response.status(502).json({
        error:
          "ارتباط با سرویس مدل برقرار نشد."
      });
    } finally {
      clearTimeout(timeoutId);
    }
  }
);

app.listen(port, () => {
  console.log(
    `Backend: http://localhost:${port}`
  );
});

تنظیم متغیرهای محیطی

فایل backend/.env:

PORT=3001
DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL_ID=YOUR_MODEL_ID

برای دریافت کلید API و شروع استفاده می‌توانید در درواره ثبت‌نام کنید.

برای مشاهده مدل‌ها، انتخاب Model ID و بررسی قیمت به‌روز هر مدل نیز صفحه مدل‌های درواره را ببینید.

کلید API را در فایل React، متغیر VITE_، Local Storage یا Repository عمومی قرار ندهید.

اجرای پروژه

Terminal اول:

cd backend
npm run dev

Terminal دوم در ریشه Frontend:

npm run dev

آدرس نمایش‌داده‌شده توسط Vite را در مرورگر باز کنید.

مدیریت Classهای شرطی

در نمونه پروژه از Template Literal استفاده کردیم:

const className = `
  rounded-xl
  ${
    isActive
      ? "bg-violet-600 text-white"
      : "bg-white text-slate-700"
  }
`;

برای پروژه بزرگ‌تر می‌توانید یک تابع کوچک بسازید:

export function cn(...values) {
  return values
    .filter(Boolean)
    .join(" ");
}

استفاده:

<button
  className={cn(
    "rounded-xl px-4 py-2",
    isActive
      ? "bg-violet-600 text-white"
      : "bg-slate-100 text-slate-700",
    disabled && "cursor-not-allowed opacity-50"
  )}
>
  انتخاب
</button>

کتابخانه‌های مدیریت Class نیز وجود دارند، اما پیش از نصب وابستگی جدید بررسی کنید آیا پیچیدگی پروژه واقعاً به آن نیاز دارد.

چه زمانی Component بسازیم؟

هر مجموعه Class طولانی الزاماً نباید به Component تبدیل شود.

Component زمانی ارزشمند است که یکی از شرایط زیر وجود داشته باشد:

  • در چند محل استفاده می‌شود.
  • رفتار مشخص دارد.
  • Variantهای مختلف دارد.
  • تست مستقل لازم دارد.
  • بخشی معنادار از رابط است.
  • تغییر آن باید در یک نقطه انجام شود.

مثال Component دکمه:

export function Button({
  variant = "primary",
  className = "",
  ...props
}) {
  const variants = {
    primary:
      "bg-darvareh-600 text-white hover:bg-darvareh-700",

    secondary:
      "bg-slate-100 text-slate-700 hover:bg-slate-200"
  };

  return (
    <button
      className={`
        min-h-11
        rounded-xl
        px-5
        py-2.5
        text-sm
        font-bold
        transition
        focus-visible:outline-none
        focus-visible:ring-4
        disabled:cursor-not-allowed
        disabled:opacity-50
        ${variants[variant]}
        ${className}
      `}
      {...props}
    />
  );
}

استفاده از @apply

Tailwind امکان ترکیب Utilityها در CSS با @apply را فراهم می‌کند:

.primary-button {
  @apply rounded-xl bg-violet-600 px-5 py-3 text-white;
}

اما استفاده زیاد از @apply می‌تواند شما را دوباره به ساخت تعداد زیادی Class اختصاصی برگرداند و بخشی از مزیت Utility-first را کاهش دهد.

موارد قابل‌قبول:

  • Style پایه عنصر مشترک
  • Component در محیطی که Markup در کنترل شما نیست
  • یکپارچه‌سازی با کتابخانه ثالث
  • الگوی محدود و واقعاً پرتکرار

برای Componentهای React معمولاً استخراج Component از ساخت Class اختصاصی عمومی‌تر است.

دسترس‌پذیری در Tailwind

Tailwind به‌صورت خودکار رابط را Accessible نمی‌کند. مسئولیت ساخت HTML معنایی و رفتار درست همچنان با توسعه‌دهنده است.

موارد مهم:

  • از button برای عملیات استفاده کنید.
  • از a برای Navigation استفاده کنید.
  • Label فرم را حذف نکنید.
  • Focus قابل‌مشاهده داشته باشید.
  • Contrast رنگ را بررسی کنید.
  • حالت Disabled واقعی تنظیم کنید.
  • فقط به رنگ برای نمایش وضعیت وابسته نباشید.
  • ترتیب Tab را بررسی کنید.
  • برای Icon Button نام قابل‌دسترسی تعریف کنید.
  • Loading را برای Screen Reader اعلام کنید.

نمونه Focus:

<button
  class="
    focus-visible:outline-none
    focus-visible:ring-4
    focus-visible:ring-violet-200
  "
>
  ارسال
</button>

Motion و ترجیح کاربر

برای محدودکردن Animation در صورت ترجیح کاربر:

<div
  class="
    motion-safe:animate-pulse
    motion-reduce:animate-none
  "
>
  ...
</div>

برای Transition:

<button
  class="
    transition
    motion-reduce:transition-none
  "
>
  ادامه
</button>

خطاهای رایج Tailwind CSS

Utilityها اعمال نمی‌شوند

بررسی کنید:

  • Tailwind نصب شده است؟
  • Plugin به Vite اضافه شده؟
  • @import "tailwindcss"; وجود دارد؟
  • فایل CSS در main.jsx Import شده؟
  • Dev Server پس از تغییر تنظیمات Restart شده؟
  • نام Class صحیح است؟
  • Class به‌صورت پویا ساخته نشده؟

نصب قدیمی را با نصب جدید ترکیب کرده‌اید

بسیاری از آموزش‌های قدیمی شامل فایل‌ها و دستورهای متفاوت هستند. روش نصب را براساس مستندات نسخه فعلی Tailwind انتخاب کنید و دستورهای چند نسخه مختلف را با هم ترکیب نکنید.

Class پویا شناسایی نمی‌شود

نام کامل Classها را در Source قرار دهید:

const sizes = {
  small: "text-sm",
  large: "text-lg"
};

نه:

const className = `text-${size}`;

Responsive برعکس انتظار عمل می‌کند

Tailwind Mobile-first است:

<div class="text-sm md:text-base lg:text-lg">
  ...
</div>

Class پایه برای موبایل است و Prefixها از Breakpoint مربوط به بالا اعمال می‌شوند.

Dark Mode کار نمی‌کند

بررسی کنید:

  • Variant مربوط به Dark Mode تعریف شده؟
  • Class برابر با dark روی عنصر درست قرار گرفته؟
  • Utility دارای dark: است؟
  • Theme پیش از Render اولیه اعمال می‌شود؟
  • Class توسط کد دیگری حذف نمی‌شود؟

Classها با هم تعارض دارند

مثال:

<div class="p-4 p-8">
  ...
</div>

از قراردادن Utilityهای متناقض اجتناب کنید. هنگام ترکیب Variantها باید مشخص باشد کدام مقدار در چه وضعیت اعمال می‌شود.

رابط روی موبایل Overflow دارد

Utilityهای زیر را بررسی کنید:

min-w-0
max-w-full
overflow-x-auto
break-words
whitespace-pre-wrap

در Grid از این الگو استفاده کنید:

<div class="grid grid-cols-[280px_minmax(0,1fr)]">
  ...
</div>

بهینه‌سازی و Build

ساخت نسخه Production:

npm run build

بررسی Build:

npm run preview

قبل از انتشار:

  • حجم Bundle را بررسی کنید.
  • صفحه موبایل را آزمایش کنید.
  • Dark Mode را بررسی کنید.
  • Classهای استفاده‌نشده و Componentهای قدیمی را حذف کنید.
  • فونت‌ها را بهینه کنید.
  • تصویرها را فشرده کنید.
  • Focus و Keyboard Navigation را بررسی کنید.
  • خطای شبکه و Timeout را آزمایش کنید.
  • Secretها را از Bundle بررسی کنید.
  • فایل .env را در Git جست‌وجو کنید.

استفاده از هوش مصنوعی برای تولید Tailwind CSS

مدل هوش مصنوعی می‌تواند Componentهای Tailwind تولید کند، اما پرامپت باید دقیق باشد.

نمونه پرامپت:

یک Component داشبورد با React و Tailwind CSS بساز.

نیازمندی‌ها:
- رابط فارسی و RTL باشد.
- از Utility Classهای نسخه فعلی Tailwind استفاده کن.
- طراحی Mobile-first باشد.
- در موبایل یک ستون و در Desktop دو ستون داشته باشد.
- Dark Mode را با dark: پشتیبانی کند.
- Focus قابل‌مشاهده برای Keyboard داشته باشد.
- از رنگ‌های تصادفی زیاد استفاده نکن.
- Classها را در Runtime با رشته ناقص نساز.
- از مقدار Arbitrary فقط در صورت نیاز استفاده کن.
- API Key در Frontend قرار نگیرد.
- JSX کامل و قابل‌اجرا ارائه کن.

خروجی مدل را از نظر این موارد بررسی کنید:

  • سازگاری با نسخه نصب‌شده
  • وجود Classهای منسوخ یا نامعتبر
  • Responsive Design
  • Dark Mode
  • Contrast
  • Focus
  • RTL
  • Overflow
  • تکرار Utilityها
  • HTML معنایی
  • عدم وجود Secret

چک‌لیست Tailwind برای پروژه واقعی

  • روش نصب با نسخه فعلی مستندات هماهنگ است.
  • Plugin صحیح به Vite اضافه شده است.
  • فایل CSS اصلی Import شده است.
  • Theme برند تعریف شده است.
  • رنگ‌های تصادفی محدود هستند.
  • Arbitrary Value فقط در صورت نیاز استفاده شده است.
  • Classها در Runtime به‌صورت ناقص ساخته نمی‌شوند.
  • طراحی Mobile-first است.
  • RTL بررسی شده است.
  • Dark Mode در تمام Componentهای اصلی تست شده است.
  • Focus قابل‌مشاهده است.
  • Contrast متن مناسب است.
  • Loading، Error و Empty State طراحی شده‌اند.
  • min-w-0 در Layoutهای لازم وجود دارد.
  • متن طولانی از صفحه خارج نمی‌شود.
  • Classهای پرتکرار به Component تبدیل شده‌اند.
  • از @apply بیش از حد استفاده نشده است.
  • API Key در Frontend نیست.
  • متغیرهای عمومی و Secret از هم جدا هستند.
  • Build نهایی بدون خطا اجرا می‌شود.

نقشه راه یادگیری Tailwind

مرحله اول: CSS پایه

  • Box Model
  • Flexbox
  • Grid
  • Responsive Design
  • Position
  • Overflow

مرحله دوم: Utilityهای اصلی

  • Spacing
  • Sizing
  • Color
  • Typography
  • Border
  • Shadow

مرحله سوم: Layout

  • Flex
  • Grid
  • Container
  • Width
  • Min Width
  • Overflow

مرحله چهارم: Variantها

  • Hover
  • Focus
  • Disabled
  • Group
  • Peer
  • Responsive

مرحله پنجم: طراحی حرفه‌ای

  • Theme Variable
  • Dark Mode
  • RTL
  • Accessibility
  • Motion
  • Design System

مرحله ششم: معماری Component

  • Class شرطی
  • Variant
  • Component قابل‌استفاده مجدد
  • جلوگیری از تکرار
  • تست رابط

سؤال‌های متداول

Tailwind CSS چیست؟

Tailwind یک فریم‌ورک CSS مبتنی بر Utility Class است که رابط با ترکیب Classهای کوچک مانند p-4، flex، bg-white و rounded-xl ساخته می‌شود.

آیا Tailwind جای CSS را می‌گیرد؟

خیر. Tailwind روی CSS ساخته شده است و برای استفاده صحیح از آن باید مفاهیم CSS را بدانید.

آیا Tailwind برای React مناسب است؟

بله. نگهداری Utilityها در کنار Component و ساخت Variantهای قابل‌استفاده مجدد، Tailwind را به گزینه‌ای مناسب برای React تبدیل می‌کند.

Tailwind بهتر است یا Bootstrap؟

Tailwind برای طراحی اختصاصی و Componentهای سفارشی انعطاف بیشتری دارد. Bootstrap برای ساخت سریع رابط با Componentهای آماده مناسب است. انتخاب به نیاز پروژه بستگی دارد.

آیا Tailwind باعث شلوغ‌شدن JSX می‌شود؟

در Componentهای پیچیده ممکن است className طولانی شود. با تقسیم درست Component، مرتب‌کردن Utilityها و ساخت Variant می‌توان این مشکل را کنترل کرد.

آیا می‌توان CSS معمولی را کنار Tailwind استفاده کرد؟

بله. می‌توانید در کنار Utilityها از CSS معمولی، CSS Module یا Styleهای اختصاصی استفاده کنید.

آیا Tailwind از Dark Mode پشتیبانی می‌کند؟

بله. با Variant برابر با dark: می‌توان Styleهای حالت تیره را تعریف کرد. فعال‌شدن آن می‌تواند براساس تنظیم سیستم یا Class اختصاصی باشد.

آیا Tailwind از RTL پشتیبانی می‌کند؟

بله. جهت اصلی در HTML با dir="rtl" تنظیم می‌شود و Utilityهای منطقی مانند ms-* و me-* برای فاصله متناسب با جهت قابل‌استفاده‌اند.

چرا Class پویا در Tailwind کار نمی‌کند؟

Tailwind باید نام کامل Class را در Source پیدا کند. رشته‌ای مانند bg-${color}-600 ممکن است قابل‌شناسایی نباشد. Classهای کامل را در Map قرار دهید.

آیا Tailwind برای سئو مناسب است؟

Tailwind مستقیماً رتبه سئو ایجاد نمی‌کند. خروجی درست HTML، سرعت، Responsive Design، دسترس‌پذیری و کیفیت محتوا عوامل مهم‌تری هستند. Tailwind می‌تواند در ساخت رابط سبک و واکنش‌گرا کمک کند.

آیا Tailwind برای پروژه بزرگ مناسب است؟

بله، به‌شرط اینکه Theme، Component، Variant و قواعد طراحی مشخص باشند. استفاده بی‌قاعده از Utilityها می‌تواند رابط را ناسازگار کند.

آیا API Key را می‌توان در پروژه React و Tailwind قرار داد؟

خیر. کد Frontend برای کاربران قابل‌مشاهده است. API Key باید در Backend یا Secret Manager نگهداری شود.

چگونه Tailwind را با Vite نصب کنیم؟

در روش فعلی، بسته‌های tailwindcss و @tailwindcss/vite نصب می‌شوند، Plugin به vite.config.js اضافه می‌شود و در CSS از @import "tailwindcss"; استفاده می‌کنیم.

آیا به فایل tailwind.config.js نیاز داریم؟

در روش جدید بسیاری از تنظیمات می‌توانند مستقیماً در CSS و با Theme Variableها انجام شوند. نیاز دقیق به فایل Config به نسخه، Pluginها و ساختار پروژه بستگی دارد. راهنمای نسخه نصب‌شده را مبنا قرار دهید.

جمع‌بندی

Tailwind CSS روشی Utility-first برای طراحی رابط وب ارائه می‌دهد. به‌جای ساخت Class اختصاصی برای هر عنصر، Utilityهای کوچک را ترکیب می‌کنیم و ظاهر Component را در کنار Markup نگه می‌داریم.

مهم‌ترین مفاهیم Tailwind عبارت‌اند از:

  • Utility Class
  • Variant
  • Responsive Design
  • Mobile-first
  • Flexbox و Grid
  • Arbitrary Value
  • Theme Variable
  • Dark Mode
  • Group و Peer
  • Class شرطی
  • Component قابل‌استفاده مجدد

در پروژه عملی این مقاله، یک اپلیکیشن پردازش متن با React و Tailwind ساختیم که طراحی فارسی، Responsive و Dark Mode دارد. Frontend درخواست را به Backend ارسال می‌کند و Backend با استفاده از کلید محرمانه به API درواره متصل می‌شود.

این معماری مانع قرارگرفتن API Key در Bundle مرورگر می‌شود و امکان اعتبارسنجی ورودی، Timeout، کنترل خطا و انتخاب Model ID را در سمت سرور فراهم می‌کند.

برای دریافت کلید API و شروع توسعه محصول هوش مصنوعی می‌توانید در درواره ثبت‌نام کنید. برای مشاهده مدل‌های قابل‌استفاده، Model ID و قیمت‌های به‌روز نیز صفحه مدل‌های درواره را ببینید.

منابع تکمیلی

مقالات مرتبط

برای مطالعه شرایط استفاده و محدودیت‌های مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.

Read more

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی با ترکیب گردش‌کارهای خودکار و مدل‌های هوش مصنوعی، پردازش متن، دسته‌بندی، استخراج اطلاعات و تصمیم‌های پیشنهادی را خودکار می‌کند. در این راهنما، معماری و ساخت نمونه عملی آن با API درواره را می‌آموزید.

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce شیوه‌ای جدید برای خرید اینترنتی است که در آن ایجنت هوش مصنوعی می‌تواند نیاز کاربر را بفهمد، محصولات را جست‌وجو و مقایسه کند و فرایند خرید را پیش ببرد. در این راهنما با معماری، UCP، ACP و پیاده‌سازی آن با API درواره آشنا می‌شوید.