בינוני · 38 דק׳ קריאה · עודכן 2026-06-22

Tailwind v4 + shadcn/ui — design system בעברית RTL

מדריך מלא להקמת design system ב-Tailwind v4 + shadcn/ui עם RTL אמיתי בעברית — התקנה, theming ב-OKLCH, logical properties, טפסים ו-dark mode.

Tailwindshadcn/uiDesign SystemRTLCSS
למי זה מתאים: מפתחים שמכירים React ו-Tailwind בסיסי, ורוצים להקים design system אמיתי בעברית — לא ״להדביק dir=rtl על ה-html ולקוות לטוב״. המדריך מניח שאתה יודע להריץ פקודות בטרמינל, מכיר npm/pnpm, ורגיל לעבוד ב-Vite או Next.js. אם Tailwind v3 הוא עדיין מה שאתה מכיר — תרגיש בבית, נסביר כל שינוי. אם אתה בונה ב-Next.js, כדאי לקרוא לצד זה את Next.js 16 App Router. ואם ה-RTL והנגישות בעברית הם הכאב המרכזי שלך, יש מדריך ייעודי על נגישות ו-RTL בעברית.

למה Tailwind + shadcn, ולא X

לפני שאתה מתקין משהו — בוא נסכים על למה. ב-2026 יש שלוש גישות עיקריות לבנות UI ב-React:

1. ספריות ״מוכנות״ סגורות — Material UI, Chakra, Mantine. אתה משלם במחיר אחד: הסטיילינג יושב בתוך הקומפוננטות שלהן, ואתה משחק במגרש שלהן. ברגע שאתה רוצה לחרוג מהדפוס — אתה נלחם ב-CSS שכבר נכתב. בעברית? תקבל RTL חלקי, ולעיתים קרובות תצטרך לפתוח issues ולחכות חצי שנה לתיקון של chevron שהולך לכיוון הלא נכון.

2. Headless בלבד — Radix UI, React Aria, Headless UI. אתה מקבל לוגיקה ו-a11y מצוין, בלי סטיילינג בכלל. עוצמה בלתי-נדלית, אבל עבודה של שבועות לפני שיש לך כפתור מסוגנן.

3. shadcn/ui + Tailwind — קומפוננטות כקוד שאתה מעתיק לפרויקט שלך. הקוד שלך, אתה הבעלים שלו. הסטייל יושב בקבצים שלך, ה-tokens יושבים ב-CSS שלך, ו-Tailwind v4 דואג שכל זה ירוץ במילישניות. הצירוף הזה הוא ברירת המחדל של 2026 לסיבה אחת: אתה לא תקוע. אתה לא תלוי בריליז של מישהו אחר כדי לתקן פוקוס-רינג שלא מתאים לזהות המותגית שלך.

הזווית של עברית. RTL הוא לא ״פיצ׳ר נוסף״ שמדביקים בסוף — הוא דורש החלטות מוקדמות: logical properties במקום physical, dir-aware variants, חצים שמתהפכים, animations slide-in שמתחלפות כיוון. shadcn/ui ב-2026 תומך ב-RTL native עם הגדרה אחת ב-components.json, ו-Tailwind v4 מציע OKLCH ו-@theme שהופכים את ה-theming לקל. הצירוף של שלוש ההחלטות הנכונות (Tailwind v4 + shadcn + RTL-first) הוא מה שהמדריך הזה מנחה. design system כזה הוא גם הבסיס שעליו אני בונה דפי נחיתה ממירים: רכיבים עקביים, tokens ממותגים, ואפס חיכוך כשמוסיפים variant חדש.

Tailwind v4 — מה חדש, מה השתנה, מה לזרוק

אם אתה מגיע מ-Tailwind v3, כמה דברים שונים בצורה דרמטית. אל תנסה לעקוף אותם — שינוי גישה.

CSS-first config — בלי tailwind.config.js

הקובץ tailwind.config.js כבר לא חובה. הקונפיגורציה יושבת ישירות בקובץ ה-CSS שלך, באמצעות @theme. זה אומר שכל ה-design tokens (צבעים, פונטים, רווחים, breakpoints) הם CSS variables אמיתיים — חיים ברנטיים, אפשר לדרוס דרך JS, אפשר להחליף theme בלי build.

@import "tailwindcss";

@theme {
  --color-brand-500: oklch(0.65 0.22 30);
  --font-sans: "Heebo", "Assistant", system-ui, sans-serif;
  --radius-base: 0.5rem;
}

ברגע שהגדרת --color-brand-500, ה-utility bg-brand-500 קיים. אוטומטית. בלי extend, בלי theme.colors. כל metric שלך הוא CSS variable.

Oxide — המנוע החדש

המנוע נכתב מחדש ב-Rust ונקרא Oxide. הביצועים — incremental builds סביב 192 מיקרושניות, פי 100 ויותר מהיר מ-v3 בפרויקטים גדולים. HMR ירד מ-340ms ל-12ms. הצורך ב-postcss.config.js נעלם — יש פלאגין Vite ייעודי (@tailwindcss/vite) ופלאגין PostCSS חדש (@tailwindcss/postcss) למקרים שצריך.

זיהוי קבצים אוטומטי

אין content: [...] כבר. Tailwind v4 מזהה את הקבצים שלך לבד — סורק את הפרויקט, מתעלם מ-.gitignore, ומוצא את כל ה-classes. אתה יכול להוסיף @source ידני אם משהו לא מתגלה (למשל קבצים ב-monorepo מחוץ ל-root), אבל ברוב המקרים — אין צורך.

OKLCH במקום RGB/HSL

הפלטה ב-v4 כתובה כולה ב-OKLCH. למה? OKLCH מספק perceptual uniformity — שינוי של 0.1 ב-L נראה אותו ״כמות שינוי״ לעין האנושית, בכל גוון. זה הופך dark mode לפשוט (תהפוך את ה-L), gradients ל-smooth, ו-contrast לחיזוי. כל ה-utilities ה-built-in (כמו bg-gray-500) הם OKLCH. אתה לא חייב להשתמש ב-OKLCH ב-tokens שלך — אבל זה הסטנדרט החדש, ואני ממליץ מאוד.

container queries מובנים

@sm, @md, @lg — variants של container queries ללא plugin. נקודה.

התקנה מאפס ב-Vite + React

נניח שאתה מתחיל מאפס. Node 20 ומעלה (Node 22 LTS מומלץ ב-2026).

צעד 1: יצירת פרויקט Vite

pnpm create vite@latest my-app --template react-ts
cd my-app
pnpm install

אני משתמש ב-pnpm לאורך המדריך — npm/yarn/bun עובדים אותו דבר, החליפו את הפקודות.

צעד 2: התקנת Tailwind v4

pnpm add tailwindcss @tailwindcss/vite

זהו. אין tailwindcss init, אין postcss.config.js.

צעד 3: עדכון vite.config.ts

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

export default defineConfig({
  plugins: [react(), tailwindcss()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
});

ה-alias @/... חשוב — shadcn מצפה לו ב-components.json.

צעד 4: קובץ ה-CSS הראשי

מחק את הקובץ src/index.css הקיים והחלף ב:

@import "tailwindcss";

@theme {
  --font-sans: "Heebo", "Assistant", system-ui, -apple-system, sans-serif;
}

html {
  font-family: var(--font-sans);
}

body {
  margin: 0;
  min-height: 100vh;
}

צעד 5: ה-HTML

עדכן את index.html:

<!DOCTYPE html>
<html lang="he" dir="rtl">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Heebo:wght@400;500;700;900&display=swap"
      rel="stylesheet"
    />
    <title>האפליקציה שלי</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

lang="he" ו-dir="rtl" הם הבסיס. בלעדיהם — RTL לא יעבוד, ו-shadcn לא ידע מה כיוון.

צעד 6: tsconfig — paths ל-alias

ב-tsconfig.json (או tsconfig.app.json בפרויקטים חדשים של Vite), הוסף:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

צעד 7: בדיקה

pnpm dev

פתח את הדפדפן, פתח את ה-DevTools, ובדוק שיש לך dir="rtl" על ה-<html>. ב-src/App.tsx כתוב משהו עם class של Tailwind והסתכל שזה עובד:

export default function App() {
  return (
    <div className="min-h-screen bg-slate-50 p-8 text-slate-900">
      <h1 className="text-4xl font-bold">שלום עולם</h1>
      <p className="mt-4 text-slate-600">
        Tailwind v4 עובד, והכיוון הוא RTL.
      </p>
    </div>
  );
}

יפה. שלב Tailwind הסתיים.

התקנה ב-Next.js 16 — גרסה מקוצרת

Next.js 16 מגיע עם תמיכה native ב-Tailwind v4 דרך ה-template (create-next-app). אם אתה כבר בפרויקט קיים:

pnpm add tailwindcss @tailwindcss/postcss

צור postcss.config.mjs:

export default {
  plugins: { "@tailwindcss/postcss": {} },
};

ב-app/globals.css:

@import "tailwindcss";

@theme {
  --font-sans: "Heebo", "Assistant", sans-serif;
}

ב-app/layout.tsx:

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="he" dir="rtl">
      <body>{children}</body>
    </html>
  );
}

זהו. השאר זהה ל-Vite — components.json, רכיבים, theming. נמשיך הלאה.

שתי נקודות RTL ספציפיות ל-Next.js. ראשית, ב-App Router ה-<DirectionProvider> של Radix חייב לשבת ב-Client Component (יש לו state) — עטוף ב-"use client" ושים אותו ב-providers.tsx שעוטף את ה-children ב-layout, לא ישירות ב-layout.tsx שהוא Server Component. שנית, אם אתה ב-SSR ומשנה theme, הוסף סקריפט קטן ב-<head> שקורא את ה-localStorage לפני ה-hydration — אחרת תקבל הבהוב של light לפני dark (FOUC). על ה-rendering וה-hydration עצמם הרחבתי ב-Next.js 16 App Router.

shadcn/ui — מה זה ולמה זה לא ״עוד ספריית קומפוננטים״

המילים ״ספריית קומפוננטים״ מטעות. shadcn/ui הוא לא ספרייה — הוא CLI שמייצר קוד בפרויקט שלך. שום דבר לא יושב ב-node_modules שלך. אין import { Button } from "shadcn-ui". הקוד מגיע אליך, נשמר ב-src/components/ui/button.tsx, ומשם אתה הבעלים.

זה מה שמשנה את הכל:

  • שינויים — חופשיים. רוצה שה-Button תקבל variant חדש בשם gradient? תפתח את הקובץ, תוסיף שורה. אין PR ל-upstream, אין לחכות לריליז.
  • גרסאות — לא קיימות. ה-Button שיצרת בינואר 2026 תישאר זהה אלא אם אתה תשנה אותו. ה-CLI לא ידחוף לך updates שיהרסו לך את המסך.
  • a11y מגיע מ-Radix UI. מאחורי הקלעים, ה-Button או ה-Dialog משתמשים ב-primitives של Radix — focus management, keyboard nav, ARIA, הכל קיים. אתה רק שותל את הסטיילינג.
  • משקל — אפס. כי הקוד שלך, ה-bundle כולל רק את מה שאתה משתמש בו.

זה לא ״best of both worlds״ — זה דרך אחרת לחשוב על קומפוננטות. אתה לא צרכן, אתה מחזיק.

התקנה: npx shadcn@latest init

עכשיו לפעולה. בשורש הפרויקט:

pnpm dlx shadcn@latest init

(או npx shadcn@latest init אם npm — אותו הדבר.)

ה-CLI יזהה את הפרויקט שלך (Vite/Next/etc) וישאל סדרה של שאלות. הנה איך לענות, ולמה:

✔ Which framework are you using? › Vite
✔ Which style would you like to use? › New York
✔ Which color would you like to use as base color? › Neutral
✔ Where is your global CSS file? › src/index.css
✔ Would you like to use CSS variables for theming? › yes
✔ Are you using a custom tailwind prefix? › no
✔ Where is your tsconfig.json? › tsconfig.json
✔ Configure import alias for components? › @/components
✔ Configure import alias for utils? › @/lib/utils
✔ Are you using React Server Components? › no  (כן ב-Next.js)
✔ Enable RTL support? › yes

שני דברים קריטיים:

  1. Enable RTL support? yes — זה מה שמפעיל את ההמרה האוטומטית של physical classes ל-logical. בלעדיו, הקוד שמועתק יהיה ml-4 במקום ms-4. אתה יכול לדלג על השאלה האינטראקטיבית ולהפעיל RTL ישר מהדגל: pnpm dlx shadcn@latest init --rtl (או create --rtl לפרויקט חדש). יש לך כבר פרויקט shadcn ותיק עם רכיבים ב-physical classes? אל תמיר ידנית — הרץ pnpm dlx shadcn@latest migrate rtl "src/components/ui/**", וה-CLI יעבור על כל הרכיבים שתואמים לתבנית וימיר אותם ל-logical בבת אחת (כולל הוספת rtl:space-x-reverse היכן שצריך). אפשר גם קובץ בודד: migrate rtl src/components/ui/button.tsx. הריצו על branch נקי ובדקו את ה-diff לפני commit.
  1. Style: New York — יותר נקי, עם פינות פחות עגולות. הסטייל ״Default״ טוב גם הוא. הבחירה אסתטית — לא מורידה יכולות.

ה-CLI ייצור את הקבצים:

src/
├── components/
│   └── ui/              # כאן יושבים רכיבי shadcn שתתקין
├── lib/
│   └── utils.ts          # מכיל את ה-cn() helper
└── index.css             # עודכן עם tokens של shadcn

components.json — קובץ הקונפיג

הקובץ שנוצר ייראה משהו כזה:

{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": false,
  "tsx": true,
  "rtl": true,
  "tailwind": {
    "css": "src/index.css",
    "baseColor": "neutral",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  },
  "iconLibrary": "lucide"
}

הדגל "rtl": true הוא ה-game changer. ברגע שהוא דלוק, כל פעם שתתקין רכיב חדש דרך ה-CLI, הקוד שמועתק יומר אוטומטית מ-physical ל-logical:

  • ml-4ms-4
  • pr-2pe-2
  • text-lefttext-start
  • left-0start-0
  • rounded-tl-lgrounded-ss-lg
  • slide-in-from-leftslide-in-from-start
  • space-x-4space-x-4 rtl:space-x-reverse

החיים שלך השתפרו ב-90%.

התקנת רכיב ראשון — Button

pnpm dlx shadcn@latest add button

ה-CLI יוסיף את הקובץ src/components/ui/button.tsx, ויתקין את התלויות הנדרשות (class-variance-authority, clsx, tailwind-merge, @radix-ui/react-slot).

שינוי חבילות חשוב. Radix איחדו את כל ה-primitives לחבילה אחת בשם radix-ui, במקום עשרות חבילות נפרדות (@radix-ui/react-dialog, @radix-ui/react-slot וכו׳). shadcn אימצו את האיחוד עם סגנון New York עוד ביוני 2025, וב-2026 זה כבר ברירת המחדל. רכיבים חדשים מ-shadcn מייבאים מ-radix-ui ישירות, למשל import { Slot } from "radix-ui". אם יש לך רכיבים ישנים שמייבאים מהחבילות הנפרדות — הם עדיין עובדים, אבל כדאי לאחד כדי לחסוך כפילויות גרסה ב-node_modules. הקוד למטה כתוב בסגנון הישן (@radix-ui/react-slot) כי הוא עדיין נפוץ בפרויקטים קיימים; הלוגיקה זהה.

נסתכל על הקוד שמועתק:

import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

const buttonVariants = cva(
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
        destructive:
          "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
        outline:
          "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
        secondary:
          "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-9 px-4 py-2",
        sm: "h-8 rounded-md px-3 text-xs",
        lg: "h-10 rounded-md px-8",
        icon: "h-9 w-9",
      },
    },
    defaultVariants: { variant: "default", size: "default" },
  }
);

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button";
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    );
  }
);
Button.displayName = "Button";

export { Button, buttonVariants };

מה קורה כאן

זה הזמן להבין את הקוד הזה לעומק, כי הוא הדפוס של כל רכיבי shadcn. כל הרכיבים בנויים על אותו שלד.

cva (Class Variance Authority). הספרייה הזו פותרת את הבעיה של ״אני רוצה כפתור עם variants״ — primary, ghost, large, small. במקום if (variant === "primary") return "bg-blue-500...", אתה מגדיר אובייקט: ״ככה זה ה-base, ככה זה ה-variant הזה״. TypeScript נותן לך autocompletion על כל ה-variants. וזה חוסם אותך מ-typos.

cn helper. ב-src/lib/utils.ts יש פונקציה קטנה:

import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

מה היא עושה? משלבת classes (כמו clsx), ואז מנקה התנגשויות של Tailwind (tailwind-merge). אם אתה מעביר cn("px-4", "px-6") — תוצאה: "px-6". בלי merge תקבל את שניהם, וה-cascade יחליט. עם merge — אתה שולט.

asChild prop. הסוד של Radix Slot — אם תעביר <Button asChild><Link to="/">בית</Link></Button>, הסטיילינג של ה-Button יוחל על ה-<a> שמתקבל מ-<Link>. אין wrapper כפול. זה מה שמאפשר composition נקי עם React Router, Next Link, וכו׳.

ה-tokens — bg-primary, text-primary-foreground. אלה לא צבעי Tailwind — אלה semantic tokens שמוגדרים ב-index.css שלך (נראה אותם מיד). שינוי --primary ב-CSS = כל הכפתורים בכל האפליקציה מתעדכנים.

השימוש בכפתור

ב-App.tsx:

import { Button } from "@/components/ui/button";

export default function App() {
  return (
    <div className="min-h-screen p-8 space-y-4">
      <h1 className="text-3xl font-bold">הכפתורים שלי</h1>
      <div className="flex flex-wrap gap-2">
        <Button>שמור</Button>
        <Button variant="secondary">בטל</Button>
        <Button variant="destructive">מחק</Button>
        <Button variant="outline">פעולה משנית</Button>
        <Button variant="ghost">פעולה שקופה</Button>
        <Button variant="link">קישור</Button>
      </div>
    </div>
  );
}

לחץ Save — RTL כבר עובד, הכפתורים מוצגים מימין לשמאל באופן טבעי, ו-gap-2 נשמר.

RTL אמיתי בעברית — הפרק החשוב

עכשיו לחלק הקשה. RTL הוא הרבה יותר מ-dir="rtl". הוא דורש שמערכת הסטיילים תדבר ב-logical properties — כלומר ״התחלה״ ו״סוף״ במקום ״ימין״ ו״שמאל״.

Logical properties — מה זה אומר

Physical (LTR-only)Logical (כיוון-מודע)בעברית
margin-left: 1remmargin-inline-start: 1remרווח לפני התוכן
padding-right: 1rempadding-inline-end: 1remפדינג אחרי התוכן
text-align: lefttext-align: startיישור לתחילת השורה
border-top-right-radiusborder-start-end-radiusפינה עליונה-סופית
left: 0inset-inline-start: 0התחלה בציר האופקי

ה-utilities של Tailwind שמייצגים את זה:

Physical classLogical class
ml-4 (margin-left)ms-4 (margin-inline-start)
mr-4me-4
pl-4ps-4
pr-4pe-4
text-lefttext-start
text-righttext-end
left-0start-0
right-0end-0
rounded-tl-lgrounded-ss-lg (start-start)
rounded-br-lgrounded-ee-lg (end-end)
border-lborder-s
border-r-2border-e-2

הכלל: אם החלפת l/r ב-s/e, אתה במסלול הנכון.

הכלל הראשון: לא לכתוב physical יותר

ברגע ש-components.json מוגדר עם "rtl": true, ה-CLI ממיר את כל הרכיבים הנכנסים. אבל הקוד שאתה כותב ידנית — שום דבר לא ימיר אותו. אם אתה כותב <div className="ml-4"> — בעברית זה ייראה שגוי.

הסכם עם הצוות: מהיום, רק ms-* ו-me-*. אם תופס physical ב-code review — דחה. תוכל לאכוף את זה עם ESLint plugin (eslint-plugin-tailwindcss) או custom rule.

Variants דירקציוניות

לפעמים אתה חייב כיוון ספציפי — למשל סמל ויזואלי שמשמעותו ״אחורה״ צריך להראות חץ שמאלי בעברית (כי ״אחורה״ בעברית = חזרה לטקסט שקראת קודם, שהוא לכיוון ימין-של-המסך-בעברית-הופך-לאחר-ההיפוך). Tailwind תומך ב-rtl: ו-ltr: variants:

<ChevronLeftIcon className="rtl:rotate-180" />

ה-icon הזה יתהפך אוטומטית בעברית. shadcn עושה זה מאליו עבור icons מוכרים — אבל אם תוסיף icons משלך, תזכור את ה-pattern הזה.

Gotchas — דברים שאתה תיתקל בהם

1. Animations. slide-in-from-left בעברית הופך ל״החלקה לכיוון הלא נכון״. פתרון: slide-in-from-start (logical). shadcn ב-RTL mode עושה את ההמרה אוטומטית. אם אתה כותב animation בעצמך, תחשוב במונחי start/end. שים לב גם ל-space-x-* — בעברית צריך להוסיף rtl:space-x-reverse כדי שהרווח האופקי בין ילדים יתהפך נכון (ה-CLI מוסיף את זה אוטומטית בהמרה).

2. Drawer / Sheet positioning. Sheet שמופיע מימין ב-LTR צריך להופיע משמאל בעברית. Radix כבר עושה את זה אוטומטית — אם DirectionProvider קיים. נכסה זאת בעוד רגע.

3. Numbers and forms. מספרים בעברית מוצגים מ-LTR גם בתוך טקסט RTL. זה בסדר — הדפדפן עושה את זה לבד עם Unicode bidi. אבל ב-<input type="tel"> תרצה לפעמים dir="ltr" ידני כדי שהמשתמש לא יתבלבל.

4. תאריכים. אותו סיפור. 15/05/2026 הוא כיוונית-LTR גם בעברית.

5. ספרות בלועזית בתוך הטקסט. ״המוצר עולה 250 ש״ח״ — הדפדפן מצליח. כשהמספר ארוך (טלפון, מק״ט), עוטף ב-<bdi>250-123-4567</bdi> או <span dir="ltr">.

6. Logical properties ב-shadow. shadow-md הוא תמיד צדדי-לא-מוטה — אז זה לא בעיה. אבל אם אתה מתאים shadow ידני עם box-shadow: -2px 0 ..., זה יישבר. השתמש ב-logical: box-shadow: var(--inset-inline-start, -2px) 0 ....

7. Bug ידוע בגרסאות Next.js 15 מוקדמות — logical properties (ms-*, me-*, וכו׳) לפעמים לא מתפרסים נכון בבילד. ב-Next.js 16 זה כבר לא צף. אם אתה עדיין תקוע ב-15.x עם הבעיה, השתמש זמנית ב-rtl:mr-* ltr:ml-* עד שתעלה גרסה.

DirectionProvider — חיוני ל-Radix

shadcn משתמש ב-Radix מתחת. Radix יודע לטפל ב-RTL רק אם יש DirectionProvider מסביב לאפליקציה. הוסף ב-main.tsx:

import { DirectionProvider } from "@radix-ui/react-direction";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./index.css";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <DirectionProvider dir="rtl">
      <App />
    </DirectionProvider>
  </StrictMode>
);

זה כל ההבדל בין dropdown שנפתח לכיוון הנכון, לבין dropdown שגולש מחוץ למסך.

checklist ל-RTL — להדפיס ולתלות מעל המסך

  • [ ] <html lang="he" dir="rtl"> במקום
  • [ ] <DirectionProvider dir="rtl"> עוטף את ה-App
  • [ ] "rtl": true ב-components.json
  • [ ] קוד שאתה כותב — רק ms-/me-/ps-/pe-/text-start/text-end/start-/end-
  • [ ] Icons עם כיווניות — rtl:rotate-180
  • [ ] שדות עם תוכן LTR (מספרי טלפון, אימייל) — dir="ltr" על האלמנט הספציפי
  • [ ] טקסטים מעורבים — בדוק שכל מספר/אנגלית נראה תקין

Theming + tokens — איך לבנות design system אמיתי

עד עכשיו תקנת רכיב והגדרת RTL. עכשיו לטוקנים — איך הופכים את הסטייל לעקבי, ניתן לשינוי, ומותאם למותג.

@theme — המקור היחיד לכל הערכים

הקובץ index.css שלך אחרי shadcn init יראה כך (קוצר):

@import "tailwindcss";

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

:root {
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);
  --accent: oklch(0.97 0 0);
  --accent-foreground: oklch(0.205 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --destructive-foreground: oklch(0.985 0 0);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);
  --radius: 0.5rem;
}

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.145 0 0);
  --card-foreground: oklch(0.985 0 0);
  --primary: oklch(0.985 0 0);
  --primary-foreground: oklch(0.205 0 0);
  /* ...וכן הלאה */
}

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-card: var(--card);
  --color-card-foreground: var(--card-foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  --color-secondary: var(--secondary);
  --color-muted: var(--muted);
  --color-muted-foreground: var(--muted-foreground);
  --color-accent: var(--accent);
  --color-destructive: var(--destructive);
  --color-border: var(--border);
  --color-input: var(--input);
  --color-ring: var(--ring);
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);
}

@layer base {
  * {
    @apply border-border;
  }
  body {
    @apply bg-background text-foreground;
  }
}

הסבר רגוע:

שכבה 1 — primitive tokens (:root). הצבעים האמיתיים, ב-OKLCH. הם נקראים ״primitives״ כי הם הערך הגולמי. שום רכיב לא משתמש בהם ישירות — הם מקור אמת.

שכבה 2 — overrides ל-dark mode (.dark). אותם משתנים, ערכים שונים. ה-@custom-variant dark (&:where(.dark, .dark *)) למעלה אומר ל-Tailwind: ״כשמופיע אבא עם class dark — תפעיל את כל ה-utilities של dark:״.

שכבה 3 — semantic tokens (@theme inline). ה-tokens שהרכיבים משתמשים בהם בפועל. --color-primary מצביע ל---primary. למה שתי שכבות? כי מחר, אם תרצה ש-primary יהיה אדום במצב מסוים — תשנה רק את --primary ב-:root, ו---color-primary בעקבותיו, וכל הרכיבים יתעדכנו אוטומטית.

ה-inline ב-@theme inline חשוב: זה אומר ל-Tailwind ״אל תיצור CSS variable חדש, השתמש ב-var() הקיים מתחת״. בלי inline, היית מקבל הכפלה.

למה OKLCH ולא HSL

ב-2026 OKLCH הוא הסטנדרט החדש. הסיבות:

  • חיזוי. שינוי L מ-0.7 ל-0.3 = הצבע נעשה כהה ב״כמות אנושית סבירה״. ב-HSL, אותו שינוי באחוז ה-L מייצר תוצאות שונות לחלוטין בין hue ל-hue.
  • טווח רחב. OKLCH מבטא צבעים שלא ניתן לבטא ב-sRGB (P3 wide gamut).
  • קל ל-dark mode. הופך L = הופך את כל הסקאלה.
  • קל ל-accessibility. אפשר לוודא contrast יחסי על ידי הפרש L בלבד.

תחשוב ב-OKLCH על LCH פשוט: L = lightness (0-1), C = chroma (saturation, 0-0.4), H = hue (0-360 מעלות).

הגדרת מותג — דוגמה מעשית

נניח שהמותג שלך הוא ״כחול חם״ + ״כתום מבטא״ + ״צ׳רקול עמוק״. זה איך זה ייראה:

:root {
  --brand-blue: oklch(0.55 0.18 240);      /* כחול קצת אפור-חם */
  --brand-orange: oklch(0.72 0.18 50);     /* כתום ברור */
  --brand-charcoal: oklch(0.25 0.02 250);  /* צ'רקול עם נגיעה כחולה */

  --background: oklch(0.99 0 0);
  --foreground: var(--brand-charcoal);
  --primary: var(--brand-blue);
  --primary-foreground: oklch(0.99 0 0);
  --accent: var(--brand-orange);
  --accent-foreground: oklch(0.15 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --border: oklch(0.92 0.01 250);
  --ring: var(--brand-blue);
  --radius: 0.625rem;
}

הרכיבים של shadcn ימשיכו לעבוד — הם רק קוראים --primary, --accent, וכו׳. אתה ממיתג את הכל בלי לגעת בקובץ של רכיב אחד.

Dark mode toggle

הוסף את הקוד הזה ל-src/hooks/use-theme.ts:

import { useEffect, useState } from "react";

type Theme = "light" | "dark" | "system";

export function useTheme() {
  const [theme, setTheme] = useState<Theme>(() => {
    if (typeof window === "undefined") return "system";
    return (localStorage.getItem("theme") as Theme) || "system";
  });

  useEffect(() => {
    const root = window.document.documentElement;
    root.classList.remove("light", "dark");

    if (theme === "system") {
      const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
        .matches
        ? "dark"
        : "light";
      root.classList.add(systemTheme);
    } else {
      root.classList.add(theme);
    }

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

  return { theme, setTheme };
}

וכפתור החלפה (src/components/theme-toggle.tsx):

import { Moon, Sun } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useTheme } from "@/hooks/use-theme";

export function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  const isDark = theme === "dark";

  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={() => setTheme(isDark ? "light" : "dark")}
      aria-label="החלף ערכת צבעים"
    >
      {isDark ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
    </Button>
  );
}

הערה ל-prefers-color-scheme. ה-hook הראשוני קורא רק פעם אחת — אם המשתמש מחליף ה-OS תוך כדי שימוש, לא תזהה. רוב הזמן זה מספיק; אם אתה רוצה reactivity מלא, תוסיף matchMedia(...).addEventListener("change", ...).

עיצוב tokens סמנטיים — מה לבנות מעבר

shadcn נותן לך תבנית התחלתית — primary, secondary, accent, muted. בפרויקט אמיתי תוסיף:

@theme inline {
  --color-success: oklch(0.7 0.15 145);
  --color-success-foreground: oklch(0.98 0 0);
  --color-warning: oklch(0.78 0.16 85);
  --color-warning-foreground: oklch(0.2 0 0);
  --color-info: oklch(0.65 0.15 230);
  --color-info-foreground: oklch(0.98 0 0);
}

עכשיו bg-success, text-warning וכו׳ קיימים אוטומטית כ-utilities. אין צורך ב-extend או הגדרה נוספת — זה כל הקסם של v4.

Form components — Input, Label, Form עם react-hook-form + zod

טפסים זה המקום שבו design system באמת מוכיח את עצמו. נראה דוגמה אמיתית — טופס יצירת קשר בעברית.

צעד 1: התקנת הרכיבים והתלויות

pnpm dlx shadcn@latest add form input label textarea button
pnpm add react-hook-form zod @hookform/resolvers

צעד 2: סכמה ב-Zod

src/schemas/contact.ts:

import { z } from "zod";

export const contactSchema = z.object({
  name: z
    .string()
    .min(2, "השם חייב להיות לפחות 2 תווים")
    .max(50, "השם ארוך מדי"),
  email: z
    .string()
    .min(1, "אימייל הוא שדה חובה")
    .email("פורמט אימייל לא תקין"),
  phone: z
    .string()
    .regex(/^0[2-9]\d{7,8}$/, "מספר טלפון לא תקין")
    .optional()
    .or(z.literal("")),
  message: z
    .string()
    .min(10, "ההודעה חייבת להיות לפחות 10 תווים")
    .max(500, "ההודעה ארוכה מדי"),
});

export type ContactFormValues = z.infer<typeof contactSchema>;

הערות חשובות לעברית:

  • כל הודעות השגיאה — בעברית, מתוך הסכמה. אל תפזר אותן ב-JSX.
  • אימייל מאומת אחרי שיש לפחות תו אחד, אחרת הודעת ״חובה״ נדחפת קודם.
  • טלפון אופציונלי — .optional().or(z.literal("")) כדי לתת ל-react-hook-form לעבוד עם ערך ריק.

צעד 3: הטופס

src/components/contact-form.tsx:

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { Button } from "@/components/ui/button";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
  contactSchema,
  type ContactFormValues,
} from "@/schemas/contact";

export function ContactForm() {
  const form = useForm<ContactFormValues>({
    resolver: zodResolver(contactSchema),
    defaultValues: {
      name: "",
      email: "",
      phone: "",
      message: "",
    },
  });

  async function onSubmit(values: ContactFormValues) {
    console.log("נשלח:", values);
    // כאן: שליחה ל-EmailJS / Firebase / API שלך
  }

  return (
    <Form {...form}>
      <form
        onSubmit={form.handleSubmit(onSubmit)}
        className="space-y-6 max-w-md"
      >
        <FormField
          control={form.control}
          name="name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>שם מלא</FormLabel>
              <FormControl>
                <Input placeholder="ישראל ישראלי" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>אימייל</FormLabel>
              <FormControl>
                <Input
                  type="email"
                  dir="ltr"
                  placeholder="[email protected]"
                  {...field}
                />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="phone"
          render={({ field }) => (
            <FormItem>
              <FormLabel>טלפון (אופציונלי)</FormLabel>
              <FormControl>
                <Input
                  type="tel"
                  dir="ltr"
                  placeholder="050-1234567"
                  {...field}
                />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="message"
          render={({ field }) => (
            <FormItem>
              <FormLabel>הודעה</FormLabel>
              <FormControl>
                <Textarea
                  rows={5}
                  placeholder="ספר לי על הפרויקט..."
                  {...field}
                />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <Button type="submit" disabled={form.formState.isSubmitting}>
          {form.formState.isSubmitting ? "שולח..." : "שלח"}
        </Button>
      </form>
    </Form>
  );
}

הפרטים שעושים את ההבדל:

  • dir="ltr" על אימייל וטלפון — כי התוכן הוא לטיני/ספרות. השדה נשאר ימני-אינטראקטיבית (label, error, placement), אבל הטקסט בפנים יזרום LTR.
  • form.formState.isSubmitting — שולט במצב הכפתור. אין צורך ב-state ידני.
  • FormMessage מציג את הודעת השגיאה מ-zod אוטומטית. אין errors.name && <span>....
  • placeholder — בעברית. תזהה את הטופס כעברית בלי לחשוב.

ה-onSubmit כאן רק כותב ל-console — בפרויקט אמיתי תחבר אותו לשירות שליחה. את החיבור המלא ל-EmailJS או Resend (כולל normalization של מספר טלפון ישראלי) פירטתי ב-טפסי יצירת קשר עם EmailJS ו-Resend.

Toast + Dialog — דפוסים נוספים

Toast — הודעות זמניות

shadcn ב-2026 משתמש ב-sonner כ-toast הרשמי (במקום ה-Toast הישן):

pnpm dlx shadcn@latest add sonner

ב-App.tsx:

import { Toaster } from "@/components/ui/sonner";

export default function App() {
  return (
    <>
      <YourRoutes />
      <Toaster
        position="top-center"
        dir="rtl"
        toastOptions={{ classNames: { toast: "text-start" } }}
      />
    </>
  );
}

ושימוש:

import { toast } from "sonner";

toast.success("הטופס נשלח בהצלחה");
toast.error("משהו השתבש, נסה שוב");
toast("הודעה ניטרלית", {
  description: "פרטים נוספים כאן",
});

ה-dir="rtl" על ה-Toaster הוא הדרך הנקייה ביותר — sonner משתמש בו לסידור פנימי של ה-action button.

Dialog — מודאל

pnpm dlx shadcn@latest add dialog
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";

export function DeleteConfirmation() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="destructive">מחק</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>אתה בטוח?</DialogTitle>
          <DialogDescription>
            פעולה זו תמחק את הנתונים לצמיתות. אי-אפשר לבטל.
          </DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <Button variant="outline">בטל</Button>
          <Button variant="destructive">כן, מחק</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

ה-Dialog ב-shadcn משתמש ב-Radix Dialog, ולכן עם DirectionProvider הוא יודע ש-X של הסגירה צריך להיות בצד שמאל בעברית (במקום ימין). זה קורה לבד.

להתקין רכיבים דרך ה-MCP server (Claude Code / Cursor)

מאז 2025 ל-shadcn יש MCP server רשמי. במקום לזכור את שם הרכיב ולהריץ add ידנית, אתה מבקש מ-Claude Code או Cursor בשפה חופשית — ״תוסיף לי טבלה עם מיון ועימוד״ — וה-agent מוצא את הרכיב הנכון ברג׳יסטרי, מריץ את ההתקנה, ומחבר אותו לקוד. זה מקצר משמעותית את ה-loop של ״צריך רכיב חדש״.

ההגדרה ב-Claude Code היא שורה אחת בטרמינל:

pnpm dlx shadcn@latest mcp init --client claude

(או --client cursor / --client vscode לכלים אחרים.) הפקודה כותבת קובץ .mcp.json בשורש הפרויקט עם ה-server מוגדר. אחרי restart לכלי, אתה יכול לבקש דברים כמו ״הראה לי אילו רכיבי form יש ב-registry״ או ״התקן את ה-dialog וה-sonner״, וה-agent מבצע ישירות. נקודה אחת ל-RTL: ה-MCP מתקין דרך אותו CLI, אז אם "rtl": true כבר ב-components.json — גם ההתקנות שה-agent מריץ יומרו ל-logical אוטומטית, בלי שתצטרך לבקש את זה במפורש.

אם אתה לא מתכנת ורוצה להבין איך לתת ל-Claude Code להרכיב לך UI שלם בלי לכתוב קוד, יש מדריך נפרד על Claude Code לעסקים — לא רק למפתחים, ועל בחירת הכלי הנכון לעבודה כתבתי ב-כלי ה-vibe coding של 2026.

מבנה פרויקט מאורגן

עם הזמן הקבצים מתרבים. הנה איך אני בונה את התיקייה ב-2026:

src/
├── components/
│   ├── ui/                     # רכיבי shadcn — אל תיגעו ידנית בלי סיבה
│   │   ├── button.tsx
│   │   ├── input.tsx
│   │   ├── form.tsx
│   │   └── dialog.tsx
│   ├── blocks/                 # קומפוזיציות — חלקי דף שבונים מ-ui
│   │   ├── hero-section.tsx
│   │   ├── contact-form.tsx
│   │   └── pricing-table.tsx
│   └── layout/                 # navbar, footer, shells
│       ├── header.tsx
│       └── footer.tsx
├── lib/
│   ├── utils.ts                # cn(), helpers
│   └── api.ts                  # קריאות API
├── hooks/
│   ├── use-theme.ts
│   └── use-media-query.ts
├── schemas/                    # סכמות Zod
│   └── contact.ts
├── locales/                    # i18n
│   ├── he.json
│   └── en.json
├── pages/                      # רק אם אתה משתמש ב-React Router
└── index.css                   # כל ה-tokens, @theme, dark mode

הכלל הקדוש: /components/ui/ הוא של shadcn, לא שלך. אם אתה כותב Button עם logic ספציפי לאפליקציה — שים ב-/components/blocks/. הפרדה זו שומרת על היכולת לעדכן רכיבי shadcn (shadcn@latest add button --overwrite) בלי לאבד את הקוד שלך.

מתי לא להשתמש ב-shadcn

shadcn הוא נהדר, אבל לא לכל דבר. סיטואציות שבהן עדיף משהו אחר:

1. אתה צריך headless בלבד, ושום סטיילינג. אם המוצר שלך הוא ספריית UI שמופצת לאחרים, או אם יש לך כבר design system פנימי שתרחיק רחוק — Radix UI direct או React Aria יותר מתאימים. shadcn מביא איתו הנחות עיצוב; אם הן לא שלך, הן מפריעות.

2. הצוות שלך לא מכיר Tailwind. shadcn = Tailwind כפילוסופיה. אם הצוות מתעקש על CSS modules או styled-components, shadcn יוצר חיכוך. בחר Mantine או Chakra במקום.

3. אתה רוצה ״הכל באוטומט״. עם shadcn אתה הבעלים — וזה אומר שאם משהו לא עובד, אתה מתקן. אם אתה צריך ספרייה שמתעדכנת אוטומטית עם תיקוני bugs, MUI יותר טוב.

4. data-heavy UIs עם טבלאות מתקדמות. ה-Table של shadcn הוא נקודת התחלה — לא AG Grid. אם אתה בונה dashboard עם 50,000 rows ו-pivot tables, אתה כנראה תרצה TanStack Table כבסיס (shadcn משתמש בו, אבל אתה תקח את זה רחוק יותר).

5. פרויקט שמשתמש ב-Vue/Svelte בלבד. יש shadcn-vue ו-shadcn-svelte — לא רעים, אבל לא ברמת הבשלות של הריאקט. תבדוק לפני שמתחייב.

טיפים מנוסים

העתק את ה-CLI אבל הבן את הקוד. אל תוסיף 50 רכיבים בבת אחת. הוסף button, השתמש שבועיים, ואז dialog, וכן הלאה. אתה רוצה להבין כל קובץ שיושב ב-/components/ui/.

אל תנעל את Tailwind בגרסה מינורית. v4 משחרר תיקוני RTL ו-Oxide בכל מספר שבועות. שמור על ^4.x ב-package.json.

OKLCH עם דגימה מ-Figma. Figma תומך ב-OKLCH מאז 2025. כשהמעצב נותן לך palette, בקש את הערכים ב-OKLCH ישירות. אם אתה ממיר עיצוב שלם מ-Figma לקוד, כתבתי על הזרימה הזו לעומק ב-מ-Figma לקוד ב-2026 — כולל איך לשמור על ה-tokens עקביים בין ה-design file לבין ה-@theme.

@theme מעל כל הגדרה אחרת. סדר חשוב: @import "tailwindcss"; ראשון, אז @custom-variant, אז @theme, אז @layer base. הפכת? משחק עם ה-cascade בלי צורך.

אחד את ה-locale. lang="he", dir="rtl", <DirectionProvider dir="rtl">, Toaster dir="rtl". אם פספסת אחד — תקבל ״כיוון מעורב״ מבלבל.

eslint-plugin-tailwindcss. עם הכלל tailwindcss/no-contradicting-classname. תופס flex flex-col flex-row מטופש.

tailwind-merge שווה זהב. ה-cn() helper הוא מה שמאפשר composition אמיתי — <Button className="bg-red-500" /> ידרוס את ה-bg-primary בלי בעיות specificity.

טבלת הכרעה — shadcn מול ספריות UI אחרות

לא משוכנע שזו הבחירה הנכונה לפרויקט שלך? הנה השוואה ישירה בין שלוש הגישות שדיברנו עליהן בהתחלה, על הצירים שבאמת משנים בעברית:

קריטריוןshadcn/ui + Tailwind v4MUI / Chakra / MantineRadix / React Aria headless
בעלות על הקודמלאה — הקוד בפרויקט שלךאפס — בתוך node_modulesחלקית — לוגיקה שלהם, סטייל שלך
RTL בעבריתnative, המרה אוטומטית של ה-CLIחלקי, תלוי בריליז שלהםלוגיקה כן, סטייל אתה בונה מאפס
מהירות התחלהבינונית — מתקין רכיב-רכיבמהירה מאוד — מיובא ומוכןאיטית — בונה הכל ידנית
גמישות עיצובמלאה — tokens ב-CSS שלךמוגבלת לתוך ה-theming שלהםבלתי-מוגבלת
משקל bundleרק מה שאתה משתמש בוהחבילה כולה, גם לא-בשימושמינימלי
a11y מובנהכן (Radix מתחת)כןכן — זה כל העניין שלהם
עדכוני bugs אוטומטייםלא — אתה הבעליםכן — npm updateכן

הכלל הפשוט: אם אתה רוצה שליטה ומיתוג עברי הדוק, לך על shadcn. אם אתה רוצה מהירות עכשיו ולא אכפת לך מהמגרש הסגור, MUI/Mantine. אם אתה בונה ספריית UI משלך לאחרים — Radix ישיר.

ביצועים ו-RTL — איפה זה יכול להישבר ב-production

הקמת design system היא רק חצי מהעבודה. לפני שאתה דוחף ל-production, שווה לבדוק שלושה דברים שספציפית ב-RTL נוטים להישבר:

1. CLS מ-fonts עבריים. Heebo ו-Assistant נטענים מ-Google Fonts. אם לא הוספת font-display: swap ו-preconnect (יש לנו אותם ב-index.html למעלה), תקבל קפיצת layout כשהפונט מתחלף — וזה פוגע ב-Cumulative Layout Shift. הוסף גם size-adjust ב-@font-face אם אתה מארח את הפונט עצמאית. הרחבתי על מדדי הליבה ב-Core Web Vitals 2026.

2. bidi בטקסט מעורב. אל תניח שהדפדפן תמיד פותר נכון. משפט כמו ״התקשר ל-03-5551234 או שלח מייל ל[email protected]״ עלול להציג את הסדר הפוך בקצוות. עטוף כל מקטע LTR ב-<bdi> או <span dir="ltr">, ובדוק על מסך אמיתי — לא רק ב-DevTools של desktop.

3. scrollbar gutter. בעברית ה-scrollbar עובר לצד שמאל. אם יש לך layout עם position: fixed שמתבסס על right: 0, הוא יתנגש. השתמש ב-inset-inline-end: 0 ותן ל-scrollbar-gutter: stable למנוע קפיצה כשתוכן ארוך נטען.

הכלל: בדוק את ה-RTL על דפדפן אמיתי, עם תוכן עברי אמיתי (לא ״lorem ipsum״), ועם DevTools של מובייל פתוח. רוב באגי ה-RTL מתגלים רק כשיש טקסט אמיתי בעמוד.

שאלות נפוצות

האם צריך tailwind.config.js ב-Tailwind v4?

לא. ב-Tailwind v4 הקונפיגורציה עברה לקובץ ה-CSS דרך הדירקטיבה @theme, וקובץ ה-JS כבר לא חובה. כל ה-design tokens (צבעים, פונטים, רווחים) הם CSS variables אמיתיים. אם אתה מהגר מ-v3, יש codemod רשמי (npx @tailwindcss/upgrade) שממיר את רוב הקונפיג הישן אוטומטית, אבל ברוב הפרויקטים החדשים פשוט מתחילים מ-@theme נקי.

shadcn/ui זה ספריית קומפוננטים שמתקינים מ-npm?

לא, וזה ההבדל המהותי. shadcn/ui הוא CLI שמעתיק קוד רכיב לתיקייה שלך (src/components/ui/), ומשם הקוד שלך לחלוטין. אין import { Button } from "shadcn-ui" ואין תלות ב-node_modules שמתעדכנת מתחתיך. היתרון הוא שליטה מלאה; המחיר הוא שתיקוני bugs לא מגיעים אוטומטית — אתה הבעלים, אתה מתחזק.

איך מפעילים RTL בעברית בלי לתקן כל class ידנית?

מגדירים "rtl": true ב-components.json (או מריצים init --rtl), ואז ה-CLI ממיר אוטומטית כל physical class ל-logical כשמתקינים רכיב — ml-4 הופך ל-ms-4, text-left ל-text-start, ו-slide-in-from-left ל-slide-in-from-start. לפרויקט קיים עם רכיבים ותיקים יש פקודה ייעודית: shadcn migrate rtl "src/components/ui/**" שעוברת על כל הרכיבים שתואמים לתבנית וממירה אותם בבת אחת. את הקוד שאתה כותב ידנית עדיין צריך לכתוב ב-logical מההתחלה.

למה DirectionProvider של Radix חיוני ל-RTL?

רכיבי shadcn בנויים על primitives של Radix (Dialog, Dropdown, Sheet), ו-Radix יודע באיזה כיוון לפתוח תפריטים ולמקם אלמנטים רק אם יש <DirectionProvider dir="rtl"> שעוטף את האפליקציה. בלעדיו, dropdown עלול להיפתח לכיוון הלא נכון ולגלוש מחוץ למסך, ו-Sheet שאמור להופיע משמאל בעברית יופיע מימין. זו הגדרה של שורה אחת שחוסכת באגי מיקום שקשה לאתר.

מה ההבדל בין ms-4 ל-ml-4, ולמה זה משנה בעברית?

ml-4 הוא physical — תמיד margin-left, בלי קשר לכיוון. ms-4 הוא logical — margin-inline-start, כלומר ״התחלת השורה״, שבעברית RTL הופך אוטומטית לצד ימין. אם תשתמש ב-ml-4 בעברית, הרווח יופיע בצד הלא נכון. הכלל הפשוט: החלף כל l/r ב-classes ב-s/e (start/end), ואתה במסלול הנכון.

למה OKLCH ולא HSL או HEX לצבעים?

OKLCH נותן perceptual uniformity — שינוי של 0.1 ב-Lightness נראה כאותה ״כמות שינוי״ לעין בכל גוון, מה שהופך dark mode ל-״תהפוך את ה-L״ פשוט. בנוסף הוא תומך בצבעי P3 wide-gamut שלא ניתן לבטא ב-sRGB, ומאפשר לוודא ניגודיות נגישה לפי הפרש L בלבד. ב-2026 כל הפלטה ה-built-in של Tailwind ושל shadcn כתובה ב-OKLCH, אז זה גם הסטנדרט שתפגוש.

אפשר להשתמש ב-shadcn/ui עם Vue או Svelte?

כן — יש shadcn-vue ו-shadcn-svelte, שעובדים על אותו עיקרון של ״קוד שמועתק לפרויקט״. הם פחות בשלים מגרסת React (פחות רכיבים, RTL לפעמים מאחור), אז כדאי לבדוק שהרכיבים שאתה צריך קיימים לפני שמתחייבים. למחסנית React + Next.js התמיכה היא המלאה ביותר, כולל ה-MCP server שמאפשר ל-Claude Code או Cursor להתקין רכיבים בשבילך.

לסיכום

design system בעברית RTL לא נשבר בגלל ה-dir="rtl" — הוא נשבר בגלל מאות החלטות קטנות של physical מול logical, של dropdown שנפתח לכיוון הלא נכון, של פונט שקופץ ב-load. הצירוף Tailwind v4 + shadcn/ui פותר את רובן בתשתית: @theme ב-OKLCH לטוקנים ממותגים, ה-CLI שממיר ל-logical אוטומטית, ו-Radix שיודע כיוון ברגע שיש DirectionProvider.

הסדר שאני ממליץ עליו: הקם Tailwind v4 (CSS-first, בלי config), הוסף shadcn עם --rtl, עטוף ב-DirectionProvider, הגדר את ה-tokens של המותג ב-OKLCH, ורק אז התחל להוסיף רכיבים — אחד-אחד, מבינים כל קובץ. אם אתה ב-Next.js, חבר את זה ל-Next.js 16 App Router, ואם נגישות בעברית קריטית לפרויקט — נגישות ו-RTL בעברית הוא הצעד הבא.

מקורות

רוצה ש-design system אמיתי יוקם לך?

בניתי משהו דומה לזה לכמה לקוחות. עברית, RTL מהרגע הראשון, tokens שמותגים נכונים, dark mode שלא נשבר. אם אתה רוצה לדלג על שעתיים-שלוש של הגדרות וללכת ישר לבנייה — דבר איתי ב-WhatsApp. 30 דקות שיחה חינם, ואני יוצא רק אם הבנתי איך לחסוך לך שבועיים.