How Styling Is Handled in Plane Frontend Applications: The Tailwind-First Architecture

Plane frontend applications use a Tailwind CSS-first architecture where utility classes are composed directly in JSX and intelligently merged through a custom cn helper powered by tailwind-merge, eliminating traditional CSS files and enabling consistent, maintainable styling across the web, admin, and live apps.

Plane is an open-source project management platform maintained by Makeplane. Understanding how styling is handled in the Plane frontend applications reveals a modern approach that prioritizes developer experience and build-time optimization. The architecture leverages Tailwind CSS processed by PostCSS, with a sophisticated class-merging pipeline that ensures utility conflicts are resolved automatically while keeping bundle sizes minimal.

The Tailwind-First Architecture

Plane’s frontend applications (web, admin, and live) do not rely on custom CSS files. Instead, the codebase adopts a utility-first methodology where styling logic lives alongside component markup.

Embedding Utilities Directly in JSX

Developers write Tailwind utility strings directly within the className prop of React components. This colocation ensures that visual styles are immediately visible when reading component code, eliminating the context-switching required when navigating between separate stylesheet and markup files. The approach requires no runtime CSS-in-JS library overhead, as Tailwind processes classes at build time via PostCSS.

Intelligent Class Resolution with cn

Long strings of utility classes become difficult to manage when handling conditional states like disabled or loading. To solve this, Plane implements a small wrapper utility called cn. This function concatenates multiple class arguments and automatically resolves conflicting utilities (e.g., p-2 p-4 resolving to the higher specificity value). The cn helper is a thin re-export from the shared @plane/utils package, working in tandem with tailwind-merge to deduplicate and merge classes intelligently.

Core Implementation Files and Utilities

The styling infrastructure is centralized in specific utility files that handle different levels of customization across Plane’s monorepo structure.

Standard Class Merging in packages/ui/src/utils/classname.tsx

The foundational styling utility resides in packages/ui/src/utils/classname.tsx. This file re-exports the cn function, making it available for all UI components. Standard components throughout the design system import { cn } from @plane/ui to compose class strings that merge base styles with conditional modifiers without manual string concatenation.

Extended Configuration in packages/propel/src/utils/classname.tsx

For applications requiring Plane-specific design tokens, the packages/propel/src/utils/classname.tsx file creates a custom Tailwind-merge instance. Using extendTailwindMerge, this configuration defines custom class groups for proprietary spacing tokens, shadows, or color scales that extend beyond standard Tailwind defaults. This allows the cn utility to understand and correctly merge custom utilities like spacing-1 or shadow-md alongside standard Tailwind classes.

The Four-Step Styling Pipeline

Plane’s styling workflow follows a predictable pipeline from development to production:

  1. Write Utilities: Developers add Tailwind utility strings directly in JSX, such as className="flex items-center gap-2", embedding styling logic within component files.
  2. Concatenate with cn: The cn helper combines static and conditional class strings, handling dynamic expressions like cn("bg-primary", isActive && "text-white") safely.
  3. Merge with tailwind-merge: The cn function internally invokes tailwind-merge (or the extended version from propel) to remove duplicate or conflicting utilities intelligently.
  4. Build Optimization: PostCSS processes the Tailwind directives at build time, tree-shaking unused utilities and generating a minimal CSS file containing only the classes actually referenced in the source code.

Because unused utilities are stripped during the build, the final CSS bundle remains lightweight regardless of the size of the Tailwind configuration.

Code Examples in Practice

The following examples demonstrate how Plane components utilize the cn utility for real-world styling scenarios:

// Simple button using the `cn` helper from @plane/ui
import { cn } from "@plane/ui";

export function PrimaryButton({
  children,
  disabled,
}: { children: React.ReactNode; disabled?: boolean }) {
  return (
    <button
      className={cn(
        "px-4 py-2 rounded-md font-medium",
        "bg-primary text-white transition-colors",
        disabled && "opacity-50 cursor-not-allowed"
      )}
      disabled={disabled}
    >
      {children}
    </button>
  );
}

For components requiring custom design tokens, Plane extends the merge configuration:

// Extending tailwind-merge for custom utilities (propel package)
import { extendTailwindMerge } from "tailwind-merge";

export const customMerge = extendTailwindMerge({
  // Add Plane-specific class groups
  classGroups: {
    shadow: [{ shadow: ["shadow-sm", "shadow-md", "shadow-lg"] }],
    spacing: ["spacing-1", "spacing-2", "spacing-3"],
  },
});

/* Usage in a component */
import { cn } from "@plane/ui";

export function Card({ className, children }: { className?: string; children: React.ReactNode }) {
  return (
    <div
      className={customMerge(
        cn("bg-surface-0 rounded-lg p-4 border border-subtle", className)
      )}
    >
      {children}
    </div>
  );
}

Summary

  • Plane frontend applications adopt a Tailwind CSS-first strategy, eliminating separate CSS files in favor of utility classes embedded in JSX.
  • The cn helper function, re-exported from @plane/ui and originating in @plane/utils, provides type-safe class concatenation using tailwind-merge under the hood.
  • packages/ui/src/utils/classname.tsx serves as the standard entry point for class merging, while packages/propel/src/utils/classname.tsx supports extended configurations for custom design tokens.
  • The build pipeline leverages PostCSS to tree-shake unused utilities, ensuring production CSS bundles contain only referenced classes for optimal performance.
  • This architecture enables rapid visual iteration—updating a Tailwind class in a component instantly reflects across the application without touching external stylesheets.

Frequently Asked Questions

Why does Plane use tailwind-merge instead of simple string concatenation?

String concatenation cannot resolve conflicts where multiple utilities target the same CSS property. According to the Plane source code, tailwind-merge intelligently deduplicates conflicting classes (e.g., merging p-2 and p-4 into just p-4) while preserving the intended override order. This prevents subtle visual bugs that would occur with template literal concatenation when conditional classes overlap.

How does Plane handle custom design tokens like spacing or colors?

Plane extends the standard Tailwind-merge behavior in packages/propel/src/utils/classname.tsx using extendTailwindMerge. This creates a custom merge instance that recognizes Plane-specific class groups—such as custom spacing tokens or shadow utilities—allowing the cn helper to correctly merge these non-standard utilities alongside standard Tailwind classes without conflicts.

Can developers use custom CSS files in Plane frontend applications?

While the architecture is optimized for utility-first styling via Tailwind, the build system supports custom CSS through the standard PostCSS pipeline. However, the Plane codebase demonstrates that all styling should ideally be handled through Tailwind utilities and the cn helper to maintain consistency across the web, admin, and live applications and to leverage automatic purging of unused styles.

What is the performance impact of the cn utility at runtime?

The cn utility introduces negligible runtime overhead. It is a thin wrapper that invokes tailwind-merge only when processing dynamic class combinations. Since Tailwind utilities are extracted at build time by PostCSS, no CSS-in-JS runtime processing occurs in the browser. The resulting CSS file contains only static, pre-computed utility classes, ensuring fast rendering without the style recalculation penalties associated with runtime CSS generation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →