We use tracking cookies to understand how you use the product and help us improve it. For more information on how we store cookies, read our  privacy policy.

Setup and Configuration

Learn how to add and configure UI components in your Plainform application

This guide covers adding new components, configuring Tailwind CSS 4, and customizing the UI system.

Adding New Components

Plainform's UI components are inspired by shadcn/ui but customized for this project. You can browse shadcn/ui's component library and adapt components to match Plainform's patterns.

Manual Component Creation

Create a new component in components/ui/:

components/ui/Badge.tsx
import * as React from 'react';
import { cn } from '@/lib/utils';

export interface IBadgeProps extends React.HTMLAttributes<HTMLDivElement> {}

export function Badge({ className, ...props }: IBadgeProps) {
  return (
    <div
      className={cn(
        'inline-flex items-center rounded-md px-2.5 py-0.5 text-xs font-semibold bg-brand text-brand-foreground',
        className
      )}
      {...props}
    />
  );
}

Note: Always prefix interfaces with I following Plainform's naming conventions.

Using Radix UI Primitives

For interactive components, use Radix UI. You can find components on shadcn/ui and adapt them:

Terminal
npm install @radix-ui/react-switch
components/ui/Switch.tsx
'use client';

import * as React from 'react';
import * as SwitchPrimitives from '@radix-ui/react-switch';
import { cn } from '@/lib/utils';

export const Switch = React.forwardRef<
  React.ElementRef<typeof SwitchPrimitives.Root>,
  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
  <SwitchPrimitives.Root
    className={cn(
      'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-brand data-[state=unchecked]:bg-neutral',
      className
    )}
    {...props}
    ref={ref}
  >
    <SwitchPrimitives.Thumb
      className={cn(
        'pointer-events-none block h-4 w-4 rounded-full bg-surface shadow-lg transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0'
      )}
    />
  </SwitchPrimitives.Root>
));

Switch.displayName = SwitchPrimitives.Root.displayName;

Tailwind CSS 4 Configuration

Theme Customization

Tailwind CSS 4 uses the @theme directive in globals.css:

components/styles/globals.css
@theme {
  /* Spacing */
  --spacing-fd-container: 1224px;
  
  /* Border radius */
  --radius: 0.625rem;
  --radius-sm: calc(var(--radius) - 4px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);
  
  /* Fonts */
  --font-poppins: var(--font-poppins);
  --font-roboto: var(--font-roboto);
  
  /* Custom colors */
  --color-surface: var(--surface);
  --color-foreground: var(--foreground);
  --color-brand: var(--brand);
  --color-neutral: var(--neutral);
}

Adding Custom Utilities

Create custom utilities in the @layer utilities block:

components/styles/globals.css
@layer utilities {
  .no-scrollbar::-webkit-scrollbar {
    display: none;
  }
  .no-scrollbar {
    -ms-overflow-style: none;
    scrollbar-width: none;
  }
  .navbar-border {
    border: 1px solid;
  }
}

Custom Animations

Define animations using @theme inline:

components/styles/globals.css
@theme inline {
  --animate-marquee: marquee var(--duration) infinite linear;
  --animate-shiny-text: shiny-text 8s infinite;

  @keyframes marquee {
    from {
      transform: translateX(0);
    }
    to {
      transform: translateX(calc(-100% - var(--gap)));
    }
  }

  @keyframes shiny-text {
    0%, 90%, 100% {
      background-position: calc(-100% - var(--shiny-width)) 0;
    }
    30%, 60% {
      background-position: calc(100% + var(--shiny-width)) 0;
    }
  }
}

Usage:

components/AnimatedText.tsx
<div className="animate-shiny-text">
  Animated text
</div>

Component Patterns

Compound Components

Create compound components for complex UI:

components/ui/Card.tsx
function Card({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      className={cn('bg-neutral/40 rounded-md border p-6', className)}
      {...props}
    />
  );
}

function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div className={cn('flex flex-col gap-1.5', className)} {...props} />
  );
}

function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div className={cn('font-semibold', className)} {...props} />
  );
}

export { Card, CardHeader, CardTitle };

Usage:

app/page.tsx
<Card>
  <CardHeader>
    <CardTitle>Card Title</CardTitle>
  </CardHeader>
</Card>

Polymorphic Components

Use asChild prop for flexible composition:

components/ui/Button.tsx
import { Slot } from '@radix-ui/react-slot';

export interface ButtonProps {
  asChild?: boolean;
}

export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : 'button';
    return <Comp ref={ref} {...props} />;
  }
);

Usage:

app/page.tsx
<Button asChild>
  <Link href="/dashboard">Go to Dashboard</Link>
</Button>

Importing Components

Direct Imports

Import components directly (recommended):

app/page.tsx
import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card';
import { Dialog } from '@/components/ui/Dialog';

Avoid Barrel Imports

Don't use barrel imports (prevents tree-shaking):

app/page.tsx
// ❌ BAD: Pulls entire module
import { Button, Card, Dialog } from '@/components/ui';

// ✅ GOOD: Direct imports enable tree-shaking
import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card';
import { Dialog } from '@/components/ui/Dialog';

Form Components

Basic Form Setup

components/forms/ContactForm.tsx
'use client';

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { Label } from '@/components/ui/Label';

const formSchema = z.object({
  email: z.string().email(),
  message: z.string().min(10)
});

type IFormData = z.infer<typeof formSchema>;

export function ContactForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<IFormData>({
    resolver: zodResolver(formSchema)
  });

  const onSubmit = async (data: IFormData) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
      <div>
        <Label htmlFor="email">Email</Label>
        <Input id="email" type="email" {...register('email')} />
        {errors.email && (
          <p className="text-sm text-red-500">{errors.email.message}</p>
        )}
      </div>
      
      <Button type="submit">Submit</Button>
    </form>
  );
}

Client vs Server Components

Client Components

Use 'use client' for interactive components:

components/ui/Dialog.tsx
'use client';

import * as DialogPrimitive from '@radix-ui/react-dialog';

export function Dialog({ ...props }) {
  return <DialogPrimitive.Root {...props} />;
}

Server Components

Keep non-interactive components as server components:

components/ui/Card.tsx
// No 'use client' directive needed

export function Card({ ...props }) {
  return <div {...props} />;
}

Next Steps

How is this guide ?

Last updated on