8.2 מודולי CSS וטיילווינד הרצאה
מודולי CSS וטיילווינד - CSS Modules and Tailwind¶
בשיעור זה נצלול לעומק לשתי גישות עיצוב פופולריות ומשלימות: CSS Modules לסגנונות מקומיים ומבודדים, ו-Tailwind CSS לעיצוב מהיר עם מחלקות שירותיות.
מודולי CSS - CSS Modules¶
מה זה CSS Modules¶
CSS Modules הם קבצי CSS רגילים שבהם כל שם מחלקה מקבל hash ייחודי בזמן build. כך נמנעות התנגשויות בין מחלקות בקומפוננטות שונות.
איך זה עובד בפועל¶
כשאנחנו כותבים:
ה-bundler (Vite) הופך את זה ל:
ובקובץ ה-TypeScript אנחנו מקבלים אובייקט:
שימוש בסיסי¶
/* Navbar.module.css */
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
height: 60px;
background-color: #1a1a2e;
color: white;
}
.logo {
font-size: 20px;
font-weight: bold;
letter-spacing: 1px;
}
.links {
display: flex;
gap: 16px;
list-style: none;
margin: 0;
padding: 0;
}
.link {
color: #ccc;
text-decoration: none;
transition: color 0.2s;
}
.link:hover {
color: white;
}
.linkActive {
color: white;
font-weight: bold;
border-bottom: 2px solid #e94560;
}
// Navbar.tsx
import styles from "./Navbar.module.css";
interface NavbarProps {
activePath: string;
}
const navLinks = [
{ path: "/", label: "בית" },
{ path: "/about", label: "אודות" },
{ path: "/contact", label: "צור קשר" },
];
function Navbar({ activePath }: NavbarProps) {
return (
<nav className={styles.navbar}>
<div className={styles.logo}>MyApp</div>
<ul className={styles.links}>
{navLinks.map((link) => (
<li key={link.path}>
<a
href={link.path}
className={`${styles.link} ${
activePath === link.path ? styles.linkActive : ""
}`}
>
{link.label}
</a>
</li>
))}
</ul>
</nav>
);
}
שילוב מחלקות מרובות¶
import clsx from "clsx";
import styles from "./Card.module.css";
interface CardProps {
variant?: "default" | "outlined" | "elevated";
compact?: boolean;
children: React.ReactNode;
}
function Card({ variant = "default", compact, children }: CardProps) {
return (
<div
className={clsx(styles.card, styles[variant], {
[styles.compact]: compact,
})}
>
{children}
</div>
);
}
- שימוש ב-clsx עם CSS Modules הוא דפוס נפוץ מאוד
styles[variant]מאפשר גישה דינמית למחלקה לפי שם
שימוש ב-composes¶
מאפשר להרכיב סגנונות ממחלקות אחרות:
/* shared.module.css */
.flexCenter {
display: flex;
align-items: center;
justify-content: center;
}
.textEllipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Avatar.module.css */
.wrapper {
composes: flexCenter from "./shared.module.css";
width: 48px;
height: 48px;
border-radius: 50%;
overflow: hidden;
background-color: #e0e0e0;
}
.name {
composes: textEllipsis from "./shared.module.css";
max-width: 120px;
font-size: 14px;
}
- composes מייבא סגנונות ממחלקות אחרות, גם מאותו קובץ וגם מקובץ אחר
- זה קורה בזמן build, לא בזמן ריצה - אין עלות ביצועים
סלקטורים גלובליים¶
לפעמים צריך לגשת לסלקטורים גלובליים מתוך CSS Module:
/* Layout.module.css */
.sidebar {
width: 250px;
background-color: #f5f5f5;
}
/* גישה לסלקטור גלובלי מתוך module */
:global(.dark-mode) .sidebar {
background-color: #2d2d2d;
}
/* הגדרת מחלקה גלובלית מתוך module */
:global(.visually-hidden) {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
}
הוספת תמיכה ב-TypeScript¶
כדי לקבל השלמה אוטומטית, צרו קובץ הצהרות:
// src/types/css.d.ts
declare module "*.module.css" {
const classes: { [key: string]: string };
export default classes;
}
טיילווינד - Tailwind CSS¶
מה זה Tailwind¶
Tailwind CSS היא ספרית utility-first CSS. במקום לכתוב CSS מותאם, משתמשים במחלקות קטנות וממוקדות ישירות באלמנטים.
התקנה בפרויקט Vite + React¶
הוסיפו את הפלאגין ל-Vite:
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [react(), tailwindcss()],
});
הוסיפו את ה-import לקובץ CSS הראשי:
מחלקות בסיסיות - ריווח¶
// Padding
<div className="p-4"> // padding: 16px (כל הכיוונים)
<div className="px-4"> // padding-left: 16px; padding-right: 16px
<div className="py-2"> // padding-top: 8px; padding-bottom: 8px
<div className="pt-6"> // padding-top: 24px
<div className="pb-8"> // padding-bottom: 32px
// Margin
<div className="m-4"> // margin: 16px
<div className="mx-auto"> // margin-left: auto; margin-right: auto
<div className="mt-2"> // margin-top: 8px
<div className="mb-6"> // margin-bottom: 24px
<div className="-mt-4"> // margin-top: -16px (שלילי)
// Gap (ב-flex/grid)
<div className="gap-4"> // gap: 16px
<div className="gap-x-2"> // column-gap: 8px
<div className="gap-y-4"> // row-gap: 16px
- סקלת הריווח: 1 = 4px, 2 = 8px, 4 = 16px, 6 = 24px, 8 = 32px
טיפוגרפיה¶
// גודל טקסט
<p className="text-xs"> // 12px
<p className="text-sm"> // 14px
<p className="text-base"> // 16px
<p className="text-lg"> // 18px
<p className="text-xl"> // 20px
<p className="text-2xl"> // 24px
<p className="text-3xl"> // 30px
// משקל
<p className="font-light"> // 300
<p className="font-normal"> // 400
<p className="font-medium"> // 500
<p className="font-semibold"> // 600
<p className="font-bold"> // 700
// רווח בין שורות
<p className="leading-tight"> // 1.25
<p className="leading-normal"> // 1.5
<p className="leading-relaxed"> // 1.625
// יישור טקסט
<p className="text-right"> // RTL - ברירת מחדל בעברית
<p className="text-center">
<p className="text-left">
צבעים¶
// צבע טקסט
<p className="text-gray-500">
<p className="text-blue-600">
<p className="text-red-500">
// צבע רקע
<div className="bg-white">
<div className="bg-gray-100">
<div className="bg-blue-500">
// צבע גבול
<div className="border border-gray-300">
<div className="border-2 border-blue-500">
// שקיפות
<div className="bg-black/50"> // רקע שחור עם 50% שקיפות
<div className="text-white/80"> // טקסט לבן עם 80% שקיפות
- לכל צבע יש סקלה מ-50 (בהיר) עד 950 (כהה)
- ניתן להוסיף שקיפות עם / ואחוזים
Flexbox¶
// כיוון
<div className="flex"> // display: flex
<div className="flex flex-col"> // flex-direction: column
<div className="flex flex-row"> // flex-direction: row (ברירת מחדל)
// יישור
<div className="flex items-center"> // align-items: center
<div className="flex justify-between"> // justify-content: space-between
<div className="flex justify-center"> // justify-content: center
<div className="flex items-start justify-end">
// גלישה
<div className="flex flex-wrap"> // flex-wrap: wrap
<div className="flex flex-nowrap"> // flex-wrap: nowrap
// גדילה וכיווץ
<div className="flex-1"> // flex: 1 1 0%
<div className="flex-none"> // flex: none
<div className="grow"> // flex-grow: 1
<div className="shrink-0"> // flex-shrink: 0
Grid¶
// הגדרת grid
<div className="grid grid-cols-3"> // 3 עמודות שוות
<div className="grid grid-cols-12"> // 12 עמודות (כמו bootstrap)
<div className="grid grid-cols-[1fr_2fr]"> // עמודות מותאמות
// פריסת אלמנטים
<div className="col-span-2"> // תופס 2 עמודות
<div className="col-span-full"> // תופס את כל השורה
<div className="col-start-2 col-end-4"> // מעמודה 2 עד 4
// דוגמה - Layout בסיסי
function Layout() {
return (
<div className="grid grid-cols-12 gap-4 p-4">
<aside className="col-span-3 bg-gray-100 p-4 rounded">
סיידבר
</aside>
<main className="col-span-9 bg-white p-4 rounded">
תוכן ראשי
</main>
</div>
);
}
עיצוב רספונסיבי¶
Tailwind משתמש בגישת mobile-first - הסגנונות חלים על כל הגדלים, ו-prefixes מוסיפים סגנונות בגדלים גדולים יותר.
// Breakpoints ברירת מחדל:
// sm: 640px
// md: 768px
// lg: 1024px
// xl: 1280px
// 2xl: 1536px
function ResponsiveGrid() {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<div className="p-4 bg-blue-100 rounded">1</div>
<div className="p-4 bg-blue-200 rounded">2</div>
<div className="p-4 bg-blue-300 rounded">3</div>
<div className="p-4 bg-blue-400 rounded">4</div>
</div>
);
}
// טקסט רספונסיבי
<h1 className="text-2xl md:text-4xl lg:text-5xl font-bold">
כותרת רספונסיבית
</h1>
// הסתרה/הצגה לפי גודל
<nav className="hidden md:flex gap-4">ניווט דסקטופ</nav>
<button className="md:hidden">תפריט המבורגר</button>
מצב כהה - Dark Mode¶
function DarkModeCard() {
return (
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow">
<h2 className="text-gray-900 dark:text-white text-xl font-bold">
כותרת
</h2>
<p className="text-gray-600 dark:text-gray-300 mt-2">
תוכן שמתאים לשני המצבים
</p>
<button className="mt-4 bg-blue-500 dark:bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-600 dark:hover:bg-blue-700">
כפתור
</button>
</div>
);
}
- ה-prefix
dark:מפעיל סגנונות כאשר ה-classdarkנמצא על אלמנט הורה (בדרך כלל html) - ניתן גם להגדיר שמצב כהה יתבסס על prefers-color-scheme של המערכת
אפקטים ומצבים¶
// Hover, Focus, Active
<button className="bg-blue-500 hover:bg-blue-600 focus:ring-2 focus:ring-blue-300 active:bg-blue-700">
כפתור
</button>
// First, Last
<li className="first:pt-0 last:pb-0 py-2 border-b last:border-b-0">
פריט ברשימה
</li>
// Group hover - כאשר ההורה ב-hover
<div className="group cursor-pointer">
<h3 className="group-hover:text-blue-500 transition-colors">כותרת</h3>
<p className="group-hover:text-gray-700 text-gray-500">תיאור</p>
</div>
// Placeholder
<input className="placeholder:text-gray-400 placeholder:italic" placeholder="הקלד כאן..." />
// Disabled
<button className="disabled:opacity-50 disabled:cursor-not-allowed bg-blue-500 text-white px-4 py-2 rounded">
כפתור
</button>
אנימציות ומעברים¶
// Transition
<div className="transition-all duration-300 ease-in-out hover:scale-105">
אלמנט עם מעבר
</div>
// Transform
<div className="hover:scale-110 hover:rotate-3 transition-transform">
אלמנט מתרחב ומסתובב
</div>
// אנימציות מובנות
<div className="animate-spin">טוען...</div>
<div className="animate-pulse">שלד טעינה</div>
<div className="animate-bounce">קופץ</div>
שיטות עבודה מומלצות - Best Practices¶
חילוץ קומפוננטות במקום @apply¶
במקום להשתמש ב-@apply ליצירת מחלקות מותאמות, עדיף לחלץ קומפוננטות ריאקט:
// לא מומלץ - @apply
// styles.css
// .btn-primary { @apply px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600; }
// מומלץ - קומפוננטה
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
variant?: "primary" | "secondary";
}
function Button({ children, onClick, variant = "primary" }: ButtonProps) {
const baseClasses = "px-4 py-2 rounded font-medium transition-colors";
const variantClasses = {
primary: "bg-blue-500 text-white hover:bg-blue-600",
secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300",
};
return (
<button className={`${baseClasses} ${variantClasses[variant]}`} onClick={onClick}>
{children}
</button>
);
}
שימוש ב-clsx עם Tailwind¶
import clsx from "clsx";
interface InputProps {
error?: string;
disabled?: boolean;
className?: string;
}
function Input({ error, disabled, className }: InputProps) {
return (
<input
disabled={disabled}
className={clsx(
"w-full px-3 py-2 border rounded-md transition-colors",
"focus:outline-none focus:ring-2",
{
"border-gray-300 focus:border-blue-500 focus:ring-blue-200": !error,
"border-red-500 focus:border-red-500 focus:ring-red-200": error,
"opacity-50 cursor-not-allowed bg-gray-100": disabled,
},
className
)}
/>
);
}
שילוב CSS Modules עם Tailwind¶
ניתן לשלב את שתי הגישות באותו פרויקט:
/* ComplexAnimation.module.css */
.animatedCard {
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
import styles from "./ComplexAnimation.module.css";
function AnimatedCard({ children }: { children: React.ReactNode }) {
return (
<div className={`${styles.animatedCard} p-4 bg-white rounded-lg shadow-md`}>
{children}
</div>
);
}
- Tailwind לסגנונות בסיסיים ומהירים
- CSS Modules לאנימציות מורכבות ודברים שקשה לעשות ב-Tailwind
דוגמה מלאה - דף נחיתה¶
import styles from "./Landing.module.css";
function LandingPage() {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
{/* Hero */}
<section className="flex flex-col items-center justify-center py-20 px-4 text-center">
<h1 className="text-4xl md:text-6xl font-bold text-gray-900 dark:text-white mb-6">
בנו אפליקציות מדהימות
</h1>
<p className="text-lg md:text-xl text-gray-600 dark:text-gray-300 max-w-2xl mb-8">
כלים מודרניים לפיתוח מהיר ואיכותי של ממשקי משתמש
</p>
<div className="flex gap-4">
<button className="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors font-medium">
התחל עכשיו
</button>
<button className="px-6 py-3 border-2 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors font-medium">
למד עוד
</button>
</div>
</section>
{/* Features */}
<section className="py-16 px-4">
<div className="max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-8">
{features.map((feature) => (
<div
key={feature.title}
className={`${styles.featureCard} p-6 bg-white dark:bg-gray-800 rounded-xl shadow-sm hover:shadow-lg transition-shadow`}
>
<div className="text-3xl mb-4">{feature.icon}</div>
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2">
{feature.title}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{feature.description}
</p>
</div>
))}
</div>
</section>
</div>
);
}
const features = [
{
icon: ">_",
title: "פשוט לשימוש",
description: "ממשק אינטואיטיבי שמאפשר להתחיל לעבוד מיד ללא הגדרות מורכבות",
},
{
icon: "{}",
title: "גמיש ומותאם",
description: "מערכת הרחבות עשירה שמאפשרת התאמה לכל צורך ודרישה",
},
{
icon: "#",
title: "ביצועים גבוהים",
description: "ארכיטקטורה מותאמת לביצועים מיטביים גם בפרויקטים גדולים",
},
];
/* Landing.module.css */
.featureCard {
animation: fadeInUp 0.5s ease-out;
}
.featureCard:nth-child(2) {
animation-delay: 0.1s;
}
.featureCard:nth-child(3) {
animation-delay: 0.2s;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
סיכום¶
- CSS Modules מספקים סגנונות מקומיים אוטומטית עם תחביר CSS סטנדרטי
- composes מאפשר שיתוף סגנונות בין מחלקות
- Tailwind CSS מציע מחלקות שירותיות לעיצוב מהיר ישירות ב-JSX
- התקנת Tailwind בפרויקט Vite פשוטה עם הפלאגין הרשמי
- Tailwind תומך ברספונסיביות מובנית עם prefixes כמו sm:, md:, lg:
- מצב כהה נתמך עם ה-prefix dark:
- עדיף לחלץ קומפוננטות ריאקט מאשר להשתמש ב-@apply
- שילוב CSS Modules עם Tailwind הוא דפוס מומלץ - Tailwind לרוב הסגנונות, modules לאנימציות ודברים מורכבים