# How to Create Custom Components Using the @plane/ui Component Library

> Learn to build custom components with the @plane/ui library. Extend interfaces and merge Tailwind styles using type-safe React primitives for flexible UI development.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: how-to-guide
- Published: 2026-06-18

---

**The @plane/ui library provides type-safe React primitives that accept `className` overrides and forward refs, allowing you to compose custom components by extending existing prop interfaces and merging Tailwind styles via the built-in `cn` utility.**

The @plane/ui package is the official React component library used throughout the Plane project management application. It offers a collection of reusable, type-safe UI primitives built on Tailwind CSS that follow consistent design patterns. When you need to create custom components using the @plane/ui component library, you leverage its architectural conventions—specifically ref forwarding, variant props, and the `cn` class merging utility—to maintain type safety and design consistency across the makeplane/plane codebase.

## Understanding the @plane/ui Architecture

Before building custom components, you need to understand how the library structures its primitives. Each component in the @plane/ui package follows a strict pattern: functional components that forward refs, accept a `className` prop for style overrides, and optionally define variant-specific props like `variant`, `size`, or `loading`.

Key architectural pieces include:

- **The `cn` utility** – Located in [`packages/ui/src/utils/classname.tsx`](https://github.com/makeplane/plane/blob/main/packages/ui/src/utils/classname.tsx), this wrapper merges Tailwind classes using a clsx-style algorithm. Every UI component uses this to concatenate default styles with user-provided `className` values.

- **Button implementation** – The [`packages/ui/src/button/button.tsx`](https://github.com/makeplane/plane/blob/main/packages/ui/src/button/button.tsx) file demonstrates the core pattern: it implements the base `<button>` element, handles variant/size logic, and forwards refs using `React.forwardRef`.

- **Type definitions** – [`packages/ui/src/button/helper.tsx`](https://github.com/makeplane/plane/blob/main/packages/ui/src/button/helper.tsx) exports `TButtonVariant` and `TButtonSize` types, plus styling functions like `getButtonStyling` that map variants to Tailwind class strings.

- **Export barrel** – The [`packages/ui/src/index.tsx`](https://github.com/makeplane/plane/blob/main/packages/ui/src/index.tsx) file re-exports all components, enabling clean imports like `import { Button, Card } from "@plane/ui"`.

## How to Create Custom Components Using @plane/ui

Building on top of Plane's UI primitives follows a consistent five-step pattern:

1. **Import UI primitives** from the `@plane/ui` workspace package.
2. **Define prop interfaces** that extend the base component's props (e.g., `interface MyCardProps extends React.HTMLAttributes<HTMLDivElement>`).
3. **Compose UI elements** by assembling primitives in your JSX, passing through variant and size props as needed.
4. **Apply custom styles** using the `className` prop combined with Tailwind utilities; the underlying `cn` helper handles merging without style conflicts.
5. **Export the component** for reuse across your application or design system.

This approach preserves TypeScript autocomplete and ensures your custom components respect the same accessibility and styling conventions as the core library.

## Practical Implementation Examples

### Composing Primitives: Building an InfoCard

The simplest pattern involves composing multiple @plane/ui primitives into a higher-level component. This example combines `Card` and `Button` to create a reusable info panel:

```tsx
// src/components/InfoCard.tsx
import React from "react";
import { Card, Button } from "@plane/ui";

export interface InfoCardProps {
  title: string;
  description: string;
  actionLabel: string;
  onAction: () => void;
  className?: string;
}

/**
 * A reusable card that shows a title, description and a primary action button.
 */
export const InfoCard: React.FC<InfoCardProps> = ({
  title,
  description,
  actionLabel,
  onAction,
  className = "",
}) => (
  <Card className={className}>
    <div className="p-4">
      <h3 className="text-xl font-medium">{title}</h3>
      <p className="mt-2 text-sm text-gray-600">{description}</p>
    </div>
    <div className="flex justify-end p-4 pt-0">
      <Button variant="primary" onClick={onAction}>
        {actionLabel}
      </Button>
    </div>
  </Card>
);

```

The `Card` component handles layout and default padding, while `Button` receives its standard `variant` prop. The wrapper passes `className` directly to `Card`, which internally calls the `cn` utility to merge styles without duplication.

### Extending Existing Components: Creating a LoadingButton

When you need to add functionality to an existing primitive, extend its prop interface. This `LoadingButton` wraps the base `Button` to add a loading state while preserving all original props:

```tsx
// src/components/LoadingButton.tsx
import React from "react";
import { Button, type ButtonProps } from "@plane/ui";

export interface LoadingButtonProps extends ButtonProps {
  /** When true, the button shows a spinner and disables user interaction */
  loading?: boolean;
}

/**
 * Wraps @plane/ui's Button to add a built-in loading state.
 */
export const LoadingButton = React.forwardRef<
  HTMLButtonElement,
  LoadingButtonProps
>(({ loading = false, children, ...rest }, ref) => (
  <Button ref={ref} disabled={loading} {...rest}>
    {loading ? (
      <span className="flex items-center space-x-2">
        <span className="animate-spin rounded-full h-4 w-4 border-b-2 border-current" />
        <span>{children}</span>
      </span>
    ) : (
      children
    )}
  </Button>
));

```

By extending `ButtonProps` from [`packages/ui/src/button/button.tsx`](https://github.com/makeplane/plane/blob/main/packages/ui/src/button/button.tsx), this component preserves all standard button options including `variant`, `size`, and `appendIcon`. The ref forwarding ensures parents can still access the underlying `<button>` element for focus management or form integration.

### Custom Layouts: AvatarBadge with Overlay Positioning

For components requiring specific visual arrangements, combine primitives with Tailwind positioning utilities. This `AvatarBadge` overlays a `Badge` component on an `Avatar`:

```tsx
// src/components/AvatarBadge.tsx
import React from "react";
import { Avatar, Badge } from "@plane/ui";

export interface AvatarBadgeProps {
  src: string;
  alt: string;
  label: string;
  badgeClassName?: string;
}

/**
 * Renders an avatar with a badge overlay positioned at the bottom-right.
 */
export const AvatarBadge: React.FC<AvatarBadgeProps> = ({
  src,
  alt,
  label,
  badgeClassName = "",
}) => (
  <div className="relative inline-block">
    <Avatar src={src} alt={alt} />
    <Badge
      className={`absolute bottom-0 right-0 transform translate-x-1/2 translate-y-1/2 ${badgeClassName}`}
    >
      {label}
    </Badge>
  </div>
);

```

Both `Avatar` and `Badge` are exported from [`packages/ui/src/index.tsx`](https://github.com/makeplane/plane/blob/main/packages/ui/src/index.tsx) and include built-in accessibility features. The wrapper uses Tailwind's `relative` and `absolute` positioning classes rather than custom CSS, maintaining consistency with the library's utility-first approach.

## Summary

- The **@plane/ui** package exposes type-safe React primitives in [`packages/ui/src/index.tsx`](https://github.com/makeplane/plane/blob/main/packages/ui/src/index.tsx) that follow consistent ref-forwarding and `className` override patterns.
- The **`cn` utility** in [`packages/ui/src/utils/classname.tsx`](https://github.com/makeplane/plane/blob/main/packages/ui/src/utils/classname.tsx) handles Tailwind class merging, preventing style conflicts when overriding defaults.
- **Component composition** involves importing primitives like `Button` and `Card`, extending their TypeScript interfaces, and assembling them with Tailwind utilities.
- **Ref forwarding** is preserved throughout the component tree, ensuring access to underlying DOM elements for form handling and focus management.
- All styling uses **Tailwind CSS** classes rather than inline styles or CSS modules, maintaining visual consistency with Plane's design system.

## Frequently Asked Questions

### What is the cn utility in @plane/ui?

The `cn` utility is a class name merging function located in [`packages/ui/src/utils/classname.tsx`](https://github.com/makeplane/plane/blob/main/packages/ui/src/utils/classname.tsx). It combines Tailwind CSS classes using a clsx-style algorithm, ensuring that default component styles and user-provided `className` props merge correctly without duplication or specificity conflicts.

### How do I extend Button props when creating a custom component?

Import the `ButtonProps` type from `@plane/ui` and extend it in your interface definition. As shown in the `LoadingButton` example, this preserves all standard button attributes—such as `variant`, `size`, and `disabled`—while allowing you to add custom props like `loading` states.

### Where are the base component styles defined in the Plane repository?

Base styling logic for components like Button resides in [`packages/ui/src/button/helper.tsx`](https://github.com/makeplane/plane/blob/main/packages/ui/src/button/helper.tsx), which exports type definitions (`TButtonVariant`, `TButtonSize`) and styling functions (`getButtonStyling`). These map prop values to specific Tailwind class strings that the component applies via the `cn` utility.

### Can I use @plane/ui components outside of the Plane application?

Yes. The library is structured as a workspace package within the monorepo, exporting components through [`packages/ui/src/index.tsx`](https://github.com/makeplane/plane/blob/main/packages/ui/src/index.tsx). You can import components using `import { Button, Card } from "@plane/ui"` in any React application that has the package installed, maintaining the same TypeScript safety and Tailwind configuration.