3 Topics
Chapter•65 Minutes

Tailwind CSS with React

Compose conditional classes, reusable variants, data-driven lists, component boundaries, and class-name utilities.

The `className` prop and conditional styles

In React, Tailwind utilities are passed through className. Use template literals or a small conditional helper to express state. Keep class candidates complete and visible in source so Tailwind can detect them. When conditions become complex, prefer a variant map or a class composition utility rather than deeply nested ternaries.

A component's class string should describe presentation; the component's props should describe meaningful behavior or state. Avoid letting arbitrary user input become a class name.

Conditional task state
function TaskRow({ task }) {
  const titleClass = task.completed
    ? "text-slate-500 line-through"
    : "text-slate-900";

  return (
    <li className="flex items-start gap-3 rounded-xl border border-slate-200 p-4">
      <span className={task.completed ? "mt-1 size-2 rounded-full bg-emerald-500" : "mt-1 size-2 rounded-full bg-amber-500"} aria-hidden="true" />
      <div className="min-w-0">
        <p className={`font-medium ${titleClass}`}>{task.title}</p>
        <p className="mt-1 text-sm text-slate-600">{task.completed ? "Completed" : "In progress"}</p>
      </div>
    </li>
  );
}
💡 Both branches use full utility strings. The component maps data state to a visual state while retaining text that communicates completion independently of color and strikethrough.
Knowledge Check

Which approach is safest for conditional Tailwind classes?

Data-driven lists and class merging

Render repeated UI from data rather than duplicating markup. Use stable keys in React lists and keep each item's semantics clear. If a component accepts caller-provided classes, understand how conflicts are resolved: blindly appending p-8 after p-4 does not guarantee the expected result because generated CSS order is not simply the order of tokens in the HTML string.

A class-merging helper can resolve known Tailwind utility conflicts, but it is an additional dependency and must be compatible with your Tailwind version. It should not be used as a substitute for a coherent component API.

Data-driven cards
const metrics = [
  { label: "Open tasks", value: 18, tone: "text-blue-700" },
  { label: "Completed", value: 42, tone: "text-emerald-700" },
  { label: "Overdue", value: 3, tone: "text-red-700" },
];

function MetricGrid() {
  return (
    <section className="grid grid-cols-1 gap-4 sm:grid-cols-3">
      {metrics.map((metric) => (
        <article key={metric.label} className="rounded-xl border border-slate-200 bg-white p-5">
          <p className="text-sm text-slate-600">{metric.label}</p>
          <p className={`mt-2 text-3xl font-bold ${metric.tone}`}>{metric.value}</p>
        </article>
      ))}
    </section>
  );
}
💡 The array keeps content separate from presentation structure. Each item uses a stable key. The tone strings are complete utilities, and the grid responds to available width.
Key Takeaways
  • Keep class candidates complete in source.
  • Use stable keys for data-driven React lists.
  • Define clear component APIs instead of relying on arbitrary class overrides.

Component variants and a small design system

A design system codifies recurring decisions: button sizes, semantic colors, radii, typography, spacing, and interaction states. Variants should be explicit and finite—for example primary, secondary, and danger—so components remain predictable and source detection remains reliable.

Do not create an abstraction for every element. Start with repeated patterns that need consistent behavior or that will be changed centrally. Document variant intent and include disabled, focus, loading, and error states where relevant.

Design for states, not just screenshots

For every reusable control, consider default, hover, focus, active, disabled, loading, success, and error states where those states apply.

Explicit button variants
const variants = {
  primary: "bg-blue-600 text-white hover:bg-blue-700",
  secondary: "border border-slate-300 bg-white text-slate-900 hover:bg-slate-50",
  danger: "bg-red-600 text-white hover:bg-red-700",
};

function ActionButton({ variant = "primary", children, ...props }) {
  const variantClass = variants[variant] ?? variants.primary;

  return (
    <button
      {...props}
      className={`rounded-lg px-4 py-2.5 text-sm font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 ${variantClass}`}
    >
      {children}
    </button>
  );
}
💡 The finite map centralizes visual choices and ensures all supported classes appear in source. In a TypeScript app, type variant from keyof typeof variants; consider whether callers should be allowed to override classes at all.