8.2 מודולי CSS וטיילווינד פתרון
פתרון - מודולי CSS וטיילווינד¶
פתרון תרגיל 1¶
/* shared.module.css */
.flexCenter {
display: flex;
align-items: center;
}
.transitionAll {
transition: all 0.3s ease;
}
/* Sidebar.module.css */
.sidebar {
composes: transitionAll from "./shared.module.css";
width: 60px;
height: 100vh;
background-color: #1a1a2e;
color: white;
display: flex;
flex-direction: column;
overflow: hidden;
position: fixed;
top: 0;
right: 0;
}
.sidebar:hover {
width: 240px;
}
.logo {
composes: flexCenter from "./shared.module.css";
composes: transitionAll from "./shared.module.css";
padding: 16px;
height: 60px;
font-size: 20px;
font-weight: bold;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
white-space: nowrap;
overflow: hidden;
}
.nav {
display: flex;
flex-direction: column;
padding: 8px;
gap: 4px;
flex: 1;
}
.link {
composes: flexCenter from "./shared.module.css";
composes: transitionAll from "./shared.module.css";
gap: 12px;
padding: 10px 16px;
border-radius: 8px;
color: #a0a0b0;
text-decoration: none;
white-space: nowrap;
overflow: hidden;
cursor: pointer;
}
.link:hover {
background-color: rgba(255, 255, 255, 0.1);
color: white;
}
.linkActive {
composes: link;
background-color: rgba(233, 69, 96, 0.2);
color: white;
border-right: 3px solid #e94560;
}
.icon {
min-width: 24px;
text-align: center;
font-size: 18px;
}
.label {
composes: transitionAll from "./shared.module.css";
opacity: 0;
font-size: 14px;
}
.sidebar:hover .label {
opacity: 1;
}
// Sidebar.tsx
import styles from "./Sidebar.module.css";
interface NavItem {
icon: string;
label: string;
path: string;
}
interface SidebarProps {
activePath: string;
onNavigate: (path: string) => void;
}
const navItems: NavItem[] = [
{ icon: "H", label: "בית", path: "/" },
{ icon: "U", label: "משתמשים", path: "/users" },
{ icon: "S", label: "הגדרות", path: "/settings" },
{ icon: "R", label: "דוחות", path: "/reports" },
];
function Sidebar({ activePath, onNavigate }: SidebarProps) {
return (
<aside className={styles.sidebar}>
<div className={styles.logo}>
<span className={styles.icon}>A</span>
<span className={styles.label}>אפליקציה</span>
</div>
<nav className={styles.nav}>
{navItems.map((item) => (
<a
key={item.path}
className={
activePath === item.path ? styles.linkActive : styles.link
}
onClick={() => onNavigate(item.path)}
>
<span className={styles.icon}>{item.icon}</span>
<span className={styles.label}>{item.label}</span>
</a>
))}
</nav>
</aside>
);
}
- composes משתף סגנונות בין מחלקות ללא כפילות
- הסיידבר מתכווץ ומתרחב עם CSS transition בלבד
- שימוש ב-overflow: hidden מונע טקסט חתוך
פתרון תרגיל 2¶
interface Post {
id: number;
title: string;
excerpt: string;
date: string;
}
const posts: Post[] = [
{ id: 1, title: "פוסט ראשון", excerpt: "תיאור קצר של הפוסט הראשון שלי", date: "2024-01-15" },
{ id: 2, title: "פוסט שני", excerpt: "תיאור קצר של הפוסט השני שלי", date: "2024-02-20" },
{ id: 3, title: "פוסט שלישי", excerpt: "תיאור קצר של הפוסט השלישי שלי", date: "2024-03-10" },
{ id: 4, title: "פוסט רביעי", excerpt: "תיאור קצר של הפוסט הרביעי שלי", date: "2024-04-05" },
{ id: 5, title: "פוסט חמישי", excerpt: "תיאור קצר של הפוסט החמישי שלי", date: "2024-05-18" },
{ id: 6, title: "פוסט שישי", excerpt: "תיאור קצר של הפוסט השישי שלי", date: "2024-06-22" },
];
function ProfilePage() {
return (
<div className="min-h-screen bg-gray-100 dark:bg-gray-900">
{/* כותרת עם תמונת כיסוי */}
<div className="relative">
<div className="h-48 md:h-64 bg-gradient-to-l from-blue-500 to-purple-600" />
<div className="absolute -bottom-16 right-8">
<div className="w-32 h-32 rounded-full border-4 border-white dark:border-gray-900 bg-gray-300 dark:bg-gray-700 flex items-center justify-center text-4xl text-gray-500">
U
</div>
</div>
</div>
{/* פרטי משתמש */}
<div className="pt-20 px-8">
<h1 className="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white">
ישראל ישראלי
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
מפתח Full Stack - אוהב לבנות דברים יפים ושימושיים
</p>
</div>
{/* סטטיסטיקות */}
<div className="flex gap-8 px-8 mt-6 pb-6 border-b border-gray-200 dark:border-gray-700">
<div className="text-center">
<div className="text-xl font-bold text-gray-900 dark:text-white">42</div>
<div className="text-sm text-gray-500 dark:text-gray-400">פוסטים</div>
</div>
<div className="text-center">
<div className="text-xl font-bold text-gray-900 dark:text-white">1.2K</div>
<div className="text-sm text-gray-500 dark:text-gray-400">עוקבים</div>
</div>
<div className="text-center">
<div className="text-xl font-bold text-gray-900 dark:text-white">350</div>
<div className="text-sm text-gray-500 dark:text-gray-400">עוקב</div>
</div>
</div>
{/* רשת פוסטים */}
<div className="p-8">
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-6">
הפוסטים שלי
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{posts.map((post) => (
<div
key={post.id}
className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm hover:shadow-md transition-shadow cursor-pointer"
>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
{post.title}
</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm mb-4">
{post.excerpt}
</p>
<span className="text-xs text-gray-400 dark:text-gray-500">
{post.date}
</span>
</div>
))}
</div>
</div>
</div>
);
}
- תמונת הפרופיל חופפת לתמונת הכיסוי עם
absoluteו--bottom-16 - רשת רספונסיבית עם grid-cols-1/2/3 לפי גודל מסך
- כל הצבעים תומכים במצב כהה עם dark: prefix
פתרון תרגיל 3¶
interface PlanFeature {
text: string;
included: boolean;
}
interface Plan {
name: string;
price: string;
period: string;
features: PlanFeature[];
popular?: boolean;
}
const plans: Plan[] = [
{
name: "בסיסי",
price: "29",
period: "חודש",
features: [
{ text: "עד 5 פרויקטים", included: true },
{ text: "אחסון 10GB", included: true },
{ text: "תמיכה באימייל", included: true },
{ text: "API גישה", included: false },
{ text: "התאמה אישית", included: false },
],
},
{
name: "פרו",
price: "79",
period: "חודש",
popular: true,
features: [
{ text: "פרויקטים ללא הגבלה", included: true },
{ text: "אחסון 100GB", included: true },
{ text: "תמיכה מועדפת", included: true },
{ text: "API גישה", included: true },
{ text: "התאמה אישית", included: false },
],
},
{
name: "ארגוני",
price: "199",
period: "חודש",
features: [
{ text: "פרויקטים ללא הגבלה", included: true },
{ text: "אחסון ללא הגבלה", included: true },
{ text: "תמיכה 24/7", included: true },
{ text: "API גישה", included: true },
{ text: "התאמה אישית מלאה", included: true },
],
},
];
function PricingCards() {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-16 px-4">
<h2 className="text-3xl md:text-4xl font-bold text-center text-gray-900 dark:text-white mb-4">
בחרו את התוכנית שלכם
</h2>
<p className="text-center text-gray-600 dark:text-gray-400 mb-12">
התחילו בחינם ושדרגו בכל עת
</p>
<div className="max-w-5xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-8 items-start">
{plans.map((plan) => (
<div
key={plan.name}
className={`relative bg-white dark:bg-gray-800 rounded-2xl p-8 shadow-sm hover:shadow-xl transition-shadow ${
plan.popular
? "border-2 border-blue-500 scale-105 md:scale-110"
: "border border-gray-200 dark:border-gray-700"
}`}
>
{plan.popular && (
<div className="absolute -top-4 right-1/2 translate-x-1/2 bg-blue-500 text-white text-sm font-medium px-4 py-1 rounded-full">
פופולרי
</div>
)}
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2">
{plan.name}
</h3>
<div className="flex items-baseline gap-1 mb-6">
<span className="text-4xl font-bold text-gray-900 dark:text-white">
{plan.price}
</span>
<span className="text-gray-500 dark:text-gray-400">
ש"ח / {plan.period}
</span>
</div>
<ul className="space-y-3 mb-8">
{plan.features.map((feature) => (
<li key={feature.text} className="flex items-center gap-2">
<span
className={
feature.included
? "text-green-500 font-bold"
: "text-gray-300 dark:text-gray-600"
}
>
{feature.included ? "V" : "X"}
</span>
<span
className={
feature.included
? "text-gray-700 dark:text-gray-300"
: "text-gray-400 dark:text-gray-500 line-through"
}
>
{feature.text}
</span>
</li>
))}
</ul>
<button
className={`w-full py-3 rounded-lg font-medium transition-colors ${
plan.popular
? "bg-blue-500 text-white hover:bg-blue-600"
: "bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"
}`}
>
{plan.popular ? "התחל עכשיו" : "בחר תוכנית"}
</button>
</div>
))}
</div>
</div>
);
}
- כרטיס הפרו גדול יותר עם scale-105/110
- תגית "פופולרי" ממוקמת עם absolute ו-negative top
- שימוש ב-line-through לתכונות לא כלולות
פתרון תרגיל 4¶
/* ResponsiveTable.module.css */
.tableContainer {
overflow-x: auto;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.table {
width: 100%;
border-collapse: collapse;
background-color: white;
}
.headerRow {
background-color: #f8f9fa;
border-bottom: 2px solid #dee2e6;
}
.headerCell {
padding: 12px 16px;
text-align: right;
font-weight: 600;
color: #333;
font-size: 14px;
}
.row {
border-bottom: 1px solid #eee;
transition: background-color 0.15s;
}
.row:hover {
background-color: #f8f9ff;
}
.row:nth-child(even) {
background-color: #fafafa;
}
.row:nth-child(even):hover {
background-color: #f0f0ff;
}
.cell {
padding: 12px 16px;
font-size: 14px;
color: #555;
}
.label {
display: none;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
@media (max-width: 768px) {
.headerRow {
display: none;
}
.table,
.table tbody,
.row,
.cell {
display: block;
width: 100%;
}
.row {
padding: 16px;
margin-bottom: 12px;
border-radius: 8px;
border: 1px solid #eee;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.row:nth-child(even) {
background-color: white;
}
.cell {
padding: 4px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.cell::before {
display: none;
}
.label {
display: block;
}
}
// ResponsiveTable.tsx
import styles from "./ResponsiveTable.module.css";
interface Employee {
name: string;
role: string;
department: string;
joinDate: string;
}
const employees: Employee[] = [
{ name: "יוסי כהן", role: "מפתח Frontend", department: "פיתוח", joinDate: "2022-03-15" },
{ name: "דנה לוי", role: "מעצבת UX", department: "עיצוב", joinDate: "2021-07-01" },
{ name: "אבי מזרחי", role: "מפתח Backend", department: "פיתוח", joinDate: "2023-01-10" },
{ name: "שירה אברהם", role: "מנהלת פרויקט", department: "ניהול", joinDate: "2020-11-20" },
];
const columns = [
{ key: "name", label: "שם" },
{ key: "role", label: "תפקיד" },
{ key: "department", label: "מחלקה" },
{ key: "joinDate", label: "תאריך הצטרפות" },
] as const;
function ResponsiveTable() {
return (
<div className={styles.tableContainer}>
<table className={styles.table}>
<thead>
<tr className={styles.headerRow}>
{columns.map((col) => (
<th key={col.key} className={styles.headerCell}>
{col.label}
</th>
))}
</tr>
</thead>
<tbody>
{employees.map((employee) => (
<tr key={employee.name} className={styles.row}>
{columns.map((col) => (
<td key={col.key} className={styles.cell}>
<span className={styles.label}>{col.label}:</span>
<span>{employee[col.key]}</span>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
- בדסקטופ: טבלה רגילה עם שורות מפוספסות ו-hover
- בטלפון: כל שורה הופכת לכרטיס עם תוויות
- ה-media query ב-768px הופך את הטבלה ל-block layout
פתרון תרגיל 5¶
import { useState } from "react";
import clsx from "clsx";
interface FormData {
name: string;
email: string;
subject: string;
message: string;
}
interface FormErrors {
name?: string;
email?: string;
subject?: string;
message?: string;
}
function ContactForm() {
const [formData, setFormData] = useState<FormData>({
name: "",
email: "",
subject: "",
message: "",
});
const [errors, setErrors] = useState<FormErrors>({});
const [touched, setTouched] = useState<Record<string, boolean>>({});
function validate(data: FormData): FormErrors {
const errs: FormErrors = {};
if (!data.name.trim()) errs.name = "שם הוא שדה חובה";
if (!data.email.trim()) errs.email = "אימייל הוא שדה חובה";
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email))
errs.email = "כתובת אימייל לא תקינה";
if (!data.subject) errs.subject = "יש לבחור נושא";
if (!data.message.trim()) errs.message = "הודעה היא שדה חובה";
else if (data.message.trim().length < 10)
errs.message = "ההודעה צריכה להכיל לפחות 10 תווים";
return errs;
}
function handleChange(field: keyof FormData, value: string) {
const updated = { ...formData, [field]: value };
setFormData(updated);
if (touched[field]) {
setErrors(validate(updated));
}
}
function handleBlur(field: string) {
setTouched((prev) => ({ ...prev, [field]: true }));
setErrors(validate(formData));
}
const currentErrors = validate(formData);
const isValid = Object.keys(currentErrors).length === 0;
function inputClasses(field: keyof FormErrors) {
return clsx(
"w-full px-4 py-3 rounded-lg border transition-colors",
"focus:outline-none focus:ring-2",
"dark:bg-gray-800 dark:text-white",
{
"border-gray-300 dark:border-gray-600 focus:border-blue-500 focus:ring-blue-200 dark:focus:ring-blue-800":
!touched[field] || !errors[field],
"border-red-500 focus:border-red-500 focus:ring-red-200 dark:focus:ring-red-800":
touched[field] && errors[field],
"border-green-500 focus:border-green-500 focus:ring-green-200 dark:focus:ring-green-800":
touched[field] && !errors[field] && formData[field],
}
);
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-12 px-4">
<div className="max-w-2xl mx-auto">
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-2 text-center">
צור קשר
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-8 text-center">
נשמח לשמוע ממך
</p>
<form className="space-y-6 bg-white dark:bg-gray-800 p-8 rounded-xl shadow-sm">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* שם */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
שם מלא
</label>
<input
type="text"
value={formData.name}
onChange={(e) => handleChange("name", e.target.value)}
onBlur={() => handleBlur("name")}
className={inputClasses("name")}
placeholder="הכנס שם מלא"
/>
{touched.name && errors.name && (
<p className="mt-1 text-sm text-red-500">{errors.name}</p>
)}
</div>
{/* אימייל */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
אימייל
</label>
<input
type="email"
value={formData.email}
onChange={(e) => handleChange("email", e.target.value)}
onBlur={() => handleBlur("email")}
className={inputClasses("email")}
placeholder="example@email.com"
/>
{touched.email && errors.email && (
<p className="mt-1 text-sm text-red-500">{errors.email}</p>
)}
</div>
</div>
{/* נושא */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
נושא
</label>
<select
value={formData.subject}
onChange={(e) => handleChange("subject", e.target.value)}
onBlur={() => handleBlur("subject")}
className={inputClasses("subject")}
>
<option value="">בחר נושא</option>
<option value="general">שאלה כללית</option>
<option value="support">תמיכה טכנית</option>
<option value="billing">חיוב ותשלום</option>
<option value="feedback">משוב</option>
</select>
{touched.subject && errors.subject && (
<p className="mt-1 text-sm text-red-500">{errors.subject}</p>
)}
</div>
{/* הודעה */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
הודעה
</label>
<textarea
rows={5}
value={formData.message}
onChange={(e) => handleChange("message", e.target.value)}
onBlur={() => handleBlur("message")}
className={`${inputClasses("message")} resize-none`}
placeholder="כתוב את ההודעה שלך..."
/>
{touched.message && errors.message && (
<p className="mt-1 text-sm text-red-500">{errors.message}</p>
)}
</div>
{/* כפתור שליחה */}
<button
type="submit"
disabled={!isValid}
className={clsx(
"w-full py-3 rounded-lg font-medium transition-colors",
{
"bg-blue-500 text-white hover:bg-blue-600": isValid,
"bg-gray-300 dark:bg-gray-600 text-gray-500 dark:text-gray-400 cursor-not-allowed":
!isValid,
}
)}
>
שלח הודעה
</button>
</form>
</div>
</div>
);
}
- ולידציה עם שלושה מצבים ויזואליים: רגיל, שגיאה (אדום), תקין (ירוק)
- שדות מוצגים בשתי עמודות בדסקטופ ואחת בטלפון
- הכפתור disabled כשהטופס לא תקין
פתרון תרגיל 6¶
/* Header.module.css */
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 60px;
background-color: white;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
z-index: 50;
animation: slideDown 0.3s ease-out;
}
.logo {
font-size: 20px;
font-weight: bold;
color: #1a1a2e;
}
.nav {
display: flex;
gap: 24px;
}
.navLink {
color: #555;
text-decoration: none;
font-size: 14px;
transition: color 0.2s;
}
.navLink:hover {
color: #007bff;
}
.hamburger {
display: none;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
}
@keyframes slideDown {
from {
transform: translateY(-100%);
}
to {
transform: translateY(0);
}
}
@media (max-width: 768px) {
.nav {
display: none;
}
.hamburger {
display: block;
}
}
/* AppSidebar.module.css */
.sidebar {
position: fixed;
top: 60px;
right: 0;
width: 250px;
height: calc(100vh - 60px);
background-color: #f8f9fa;
border-left: 1px solid #e5e7eb;
padding: 16px;
transform: translateX(0);
transition: transform 0.3s ease;
z-index: 40;
}
.sidebarClosed {
transform: translateX(100%);
}
.overlay {
position: fixed;
inset: 0;
top: 60px;
background-color: rgba(0, 0, 0, 0.5);
z-index: 30;
animation: fadeIn 0.2s ease;
}
.sidebarLink {
display: block;
padding: 10px 12px;
border-radius: 6px;
color: #555;
text-decoration: none;
transition: background-color 0.15s;
font-size: 14px;
}
.sidebarLink:hover {
background-color: #e9ecef;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@media (min-width: 769px) {
.sidebar {
transform: translateX(0) !important;
}
.overlay {
display: none;
}
}
// AppLayout.tsx
import { useState } from "react";
import headerStyles from "./Header.module.css";
import sidebarStyles from "./AppSidebar.module.css";
function AppLayout() {
const [sidebarOpen, setSidebarOpen] = useState(false);
return (
<div className="min-h-screen bg-gray-50">
{/* Header - CSS Modules */}
<header className={headerStyles.header}>
<div className={headerStyles.logo}>MyApp</div>
<nav className={headerStyles.nav}>
<a href="/" className={headerStyles.navLink}>בית</a>
<a href="/about" className={headerStyles.navLink}>אודות</a>
<a href="/contact" className={headerStyles.navLink}>צור קשר</a>
</nav>
<button
className={headerStyles.hamburger}
onClick={() => setSidebarOpen(!sidebarOpen)}
>
=
</button>
</header>
{/* Sidebar - CSS Modules */}
{sidebarOpen && (
<div
className={sidebarStyles.overlay}
onClick={() => setSidebarOpen(false)}
/>
)}
<aside
className={`${sidebarStyles.sidebar} ${
!sidebarOpen ? sidebarStyles.sidebarClosed : ""
}`}
>
<a href="#" className={sidebarStyles.sidebarLink}>דשבורד</a>
<a href="#" className={sidebarStyles.sidebarLink}>משתמשים</a>
<a href="#" className={sidebarStyles.sidebarLink}>הגדרות</a>
<a href="#" className={sidebarStyles.sidebarLink}>דוחות</a>
</aside>
{/* תוכן ראשי - Tailwind */}
<main className="pt-[60px] md:pr-[250px] p-6">
<h1 className="text-2xl font-bold text-gray-800 mb-6">דשבורד</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{[1, 2, 3, 4, 5, 6].map((n) => (
<div
key={n}
className="bg-white rounded-lg p-6 shadow-sm hover:shadow-md transition-shadow"
>
<h3 className="text-lg font-semibold text-gray-800 mb-2">
כרטיסייה {n}
</h3>
<p className="text-gray-500 text-sm">
תוכן לדוגמה של הכרטיסייה
</p>
</div>
))}
</div>
</main>
{/* Footer - Tailwind */}
<footer className="md:pr-[250px] bg-white border-t border-gray-200 py-6 px-6 text-center text-sm text-gray-500">
כל הזכויות שמורות 2024
</footer>
</div>
);
}
- Header ו-Sidebar עם CSS Modules בגלל אנימציות מורכבות
- תוכן ראשי ו-Footer עם Tailwind לפשטות
- בטלפון הסיידבר מוסתר ומופיע עם כפתור המבורגר
תשובות לשאלות¶
-
composes וכפילות - composes לא יוצר כפילות CSS. הוא פשוט מוסיף את שמות המחלקות המיובאות לאלמנט ב-HTML. אם מחלקה A עושה composes ל-B, האלמנט יקבל את שתי המחלקות, וה-CSS של B נכתב פעם אחת בלבד.
-
sm: לעומת max-sm: -
sm:הוא mobile-first ומפעיל סגנונות מ-640px ומעלה.max-sm:מפעיל סגנונות עד 640px בלבד. נשתמש ב-sm:כשרוצים להוסיף סגנונות למסכים גדולים (הדרך המומלצת), וב-max-sm:כשרוצים להגדיר משהו רק לטלפונים. -
CSS קטן ב-Tailwind - Tailwind סורק את קבצי הפרויקט ומייצר CSS רק למחלקות שבאמת בשימוש (tree-shaking). אם לא השתמשתם ב-
text-purple-700, הוא לא ייכלל בקובץ הסופי. לכן גודל הקובץ קטן. -
מתי CSS Module ומתי Tailwind - CSS Modules מתאימים לאנימציות מורכבות, keyframes, סלקטורים מקוננים ומורכבים, ומצבים שקשה לבטא ב-Tailwind. Tailwind מתאים לרוב הסגנונות היומיומיים - spacing, colors, flexbox, typography. הם משתלבים יפה יחד.
-
group-hover: לעומת CSS רגיל -
group-hover:מאפשר להגדיר את האפקט ישירות על אלמנט הילד בלי צורך לכתוב CSS עם סלקטור מורכב כמו.parent:hover .child. זה שומר על הלוגיקה הויזואלית צמודה לאלמנט עצמו ומקל על הבנת הקוד.