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/:
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:
npm install @radix-ui/react-switch'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:
@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:
@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:
@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:
<div className="animate-shiny-text">
Animated text
</div>Component Patterns
Compound Components
Create compound components for complex UI:
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:
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
</CardHeader>
</Card>Polymorphic Components
Use asChild prop for flexible composition:
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:
<Button asChild>
<Link href="/dashboard">Go to Dashboard</Link>
</Button>Importing Components
Direct Imports
Import components directly (recommended):
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):
// ❌ 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
'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:
'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:
// No 'use client' directive needed
export function Card({ ...props }) {
return <div {...props} />;
}Next Steps
- Theming - Customize colors and styles
- Troubleshooting - Common issues and solutions
How is this guide ?
Last updated on