WebProsHub Free UI Component Library
Drop-in Blade components tuned for fast product UI. Explore live previews, inspect source, and copy usage snippets in one go.
25+
Reusable components
100%
Copy & paste ready
Tailwind 4
Optimized styling
Component stacks
Choose your build surface
Switch between Blade-ready snippets and playbooks for React TypeScript or Vue 3 composition setups.
Cards
Flexible content containers with optional header and footer regions.
Simple card
Composable container with just the essentials.
Simple card
This is a basic card with text content.
Card with subtitle
Add supporting context without clutter.
Card with subtitle
Add context right up front.
Use the subtitle prop for extra detail.
Card with footer
Footers are perfect for actions and summaries.
Card with footer
Footers are perfect for buttons or secondary text.
Forms
Composable inputs with validation states and helpers.
Stacked form
A complete contact form built with Blade components.
Input with validation error
Show inline errors with a single prop.
This email is already taken
Input with help text
Guide users with lightweight helper copy.
Choose something unique and memorable.
Alerts
Status banners with optional dismiss controls.
Alert styles
Color-coded alert blocks with optional dismiss support.
This is an informational alert message.
Operation completed successfully!
Please review your input before submitting.
An error occurred. Please try again.
This alert can be dismissed by clicking the close button.
Badges
Quick labels for statuses, tags, and counts.
Badge variants
Easily differentiate statuses or categories.
Badge sizes
Adjust sizing to match surrounding UI.
Tables
Responsive data tables with optional striping and hover states.
Basic table
A clean table with subtle dividers.
| Name | Role | Status | |
|---|---|---|---|
| John Doe | john@example.com | Admin | Active |
| Jane Smith | jane@example.com | User | Active |
Striped & hover table
Improve readability with zebra striping and hover feedback.
| Product | Price | Stock |
|---|---|---|
| Laptop | $999 | 15 |
| Mouse | $29 | 42 |
Modals
Responsive modals in three convenient sizes.
Modal sizes
Launch modals of varying sizes with one set of triggers.
Other components
Avatars, loaders, empty states, and dividers.
Avatars
Display user initials or uploaded photos.
Spinners
Accessible loading indicators.
Empty state
Friendly placeholder when lists are empty.
No items found
Get started by creating a new item.
Dividers
Break up content with horizontal or labeled dividers.
Design system workflow for React 18 + Vite
Scaffold a typed component library that mirrors the Blade implementationsโsame tokens, same UX specs, lighter SPA footprint.
1. Kickstart
// Create a new project
npm create vite@latest webproshub-react -- --template react-ts
// Navigate to the project directory
cd webproshub-react
npm install tailwindcss @tailwindcss/vite
npx tailwindcss init -p
Map Tailwind tokens to the Blade palette so both stacks stay visually in sync.
2. Vite Config
Update the vite.config.ts file to include the Tailwind CSS plugin.
import { defineConfig } from "vite"
import tailwindcss from "@tailwindcss/vite"
import path from "path"
export default defineConfig({
plugins: [tailwindcss(), react()],
resolve: { alias: { "@": path.resolve(__dirname, "./src") } },
});
Resolve paths to the src directory.
3. Tailwind CSS
Create a new file called src/index.css
@import "tailwindcss";
Import Tailwind CSS into the src/index.css file.
4. tsconfig.json
Update the tsconfig.json file to include the baseUrl and paths properties.
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
// tsconfig.app.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
Resolve paths to the src directory.
2. Components
Button primitive
Mirrors the Blade button tokens using class-variance-authority for ergonomic variants.
import { cva, type VariantProps } from 'class-variance-authority';
import type { ButtonHTMLAttributes, FC } from 'react';
const button = cva(
'inline-flex items-center justify-center rounded-full font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
{
variants: {
variant: {
primary: 'bg-[#F53003] text-white hover:bg-[#d72802]',
secondary: 'bg-white text-[#1b1b18] border border-[#e3e3e0]',
ghost: 'bg-transparent text-[#1b1b18] hover:bg-[#FDFDFC] dark:text-[#EDEDEC] dark:hover:bg-[#161615]'
},
size: {
sm: 'px-3.5 py-2 text-xs',
md: 'px-5 py-3 text-sm',
lg: 'px-6 py-3.5 text-base'
}
},
defaultVariants: { variant: 'primary', size: 'md' }
}
);
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> &
VariantProps<typeof button>;
export const Button: FC<ButtonProps> = ({ className, variant, size, ...props }) => (
<button className={button({ variant, size, className })} {...props} />
);
Card layout shell
Composable section wrapper that shares the glassmorphism styling of our Blade cards.
import type { FC, ReactNode } from 'react';
import { cn } from './utils/cn';
interface CardProps {
title: string;
subtitle?: string;
footer?: ReactNode;
className?: string;
children: ReactNode;
}
export const Card: FC<CardProps> = ({ title, subtitle, footer, className, children }) => (
<section
className={cn(
'rounded-2xl border border-[#e3e3e0] bg-[#FDFDFC] p-6 shadow-sm dark:border-[#2a2a29] dark:bg-[#141413]',
className
)}
>
<header className='space-y-1'>
<h3 className='text-lg font-semibold text-[#1b1b18] dark:text-[#EDEDEC]'>{title}</h3>
{subtitle ? <p className='text-sm text-[#706f6c] dark:text-[#A1A09A]'>{subtitle}</p> : null}
</header>
<div className='mt-4 space-y-4 text-sm text-[#41403d] dark:text-[#C7C6C2]'>{children}</div>
{footer ? <footer className='mt-6 pt-4 border-t border-[#e3e3e0]/60 dark:border-[#2a2a29]/60'>{footer}</footer> : null}
</section>
);
Contact form flow
React Hook Form + Zod for the same validation UX we ship in Blade.
import { Controller, useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
const schema = z.object({
name: z.string().min(2, 'Name is required'),
email: z.string().email('Valid email required'),
message: z.string().min(10, 'Tell me more about the project'),
});
type FormValues = z.infer<typeof schema>;
export function ContactForm() {
const {
control,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormValues>({ resolver: zodResolver(schema) });
const onSubmit = async (values: FormValues) => {
await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values),
});
};
return (
<form className='space-y-4' onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name='name'
render={({ field }) => (
<label className='block space-y-2'>
<span className='text-sm font-medium text-[#1b1b18] dark:text-[#EDEDEC]'>Full name</span>
<input
{...field}
className='w-full rounded-xl border border-[#e3e3e0] bg-white px-4 py-3 text-sm text-[#1b1b18] placeholder:text-[#A1A09A] focus:border-[#526DFF] focus:outline-none focus:ring-2 focus:ring-[#526DFF]/40 dark:border-[#2a2a29] dark:bg-[#141413] dark:text-[#EDEDEC]'
placeholder='Alex Mercer'
/>
{errors.name ? <span className='text-xs text-[#F53003]'>{errors.name.message}</span> : null}
</label>
)}
/>
<Controller
control={control}
name='email'
render={({ field }) => (
<label className='block space-y-2'>
<span className='text-sm font-medium text-[#1b1b18] dark:text-[#EDEDEC]'>Email</span>
<input
{...field}
type='email'
className='w-full rounded-xl border border-[#e3e3e0] bg-white px-4 py-3 text-sm text-[#1b1b18] placeholder:text-[#A1A09A] focus:border-[#526DFF] focus:outline-none focus:ring-2 focus:ring-[#526DFF]/40 dark:border-[#2a2a29] dark:bg-[#141413] dark:text-[#EDEDEC]'
placeholder='you@company.com'
/>
{errors.email ? <span className='text-xs text-[#F53003]'>{errors.email.message}</span> : null}
</label>
)}
/>
<Controller
control={control}
name='message'
render={({ field }) => (
<label className='block space-y-2'>
<span className='text-sm font-medium text-[#1b1b18] dark:text-[#EDEDEC]'>Project brief</span>
<textarea
{...field}
rows={4}
className='w-full rounded-xl border border-[#e3e3e0] bg-white px-4 py-3 text-sm text-[#1b1b18] placeholder:text-[#A1A09A] focus:border-[#526DFF] focus:outline-none focus:ring-2 focus:ring-[#526DFF]/40 dark:border-[#2a2a29] dark:bg-[#141413] dark:text-[#EDEDEC]'
placeholder='Share goals, timelines, required deliverables...'
/>
{errors.message ? <span className='text-xs text-[#F53003]'>{errors.message.message}</span> : null}
</label>
)}
/>
<button
type='submit'
disabled={isSubmitting}
className='inline-flex w-full items-center justify-center rounded-full bg-[#526DFF] px-6 py-3 text-sm font-semibold text-white transition hover:bg-[#3D4EEB] disabled:opacity-60 md:w-auto'
>
{isSubmitting ? 'Sending...' : 'Book a discovery call'}
</button>
</form>
);
}
Alert banner system
Tone-based styling so React alerts map 1:1 with Blade notice variants.
import { cn } from './utils/cn';
interface AlertProps {
tone?: 'info' | 'success' | 'warning' | 'danger';
title: string;
message: string;
dismissible?: boolean;
onClose?: () => void;
}
const toneStyles: Record<Exclude<AlertProps['tone'], undefined>, string> = {
info: 'bg-[#EEF2FF] text-[#1F2937] border-[#C7D2FE]',
success: 'bg-[#ECFDF5] text-[#064E3B] border-[#A7F3D0]',
warning: 'bg-[#FFFBEB] text-[#78350F] border-[#FDE68A]',
danger: 'bg-[#FEF2F2] text-[#991B1B] border-[#FECACA]',
};
export function Alert({ tone = 'info', title, message, dismissible, onClose }: AlertProps) {
return (
<aside className={cn('rounded-2xl border px-5 py-4 shadow-sm', toneStyles[tone])}>
<div className='flex items-start gap-3'>
<div className='flex-1 space-y-1'>
<p className='text-sm font-semibold uppercase tracking-[0.3em]'>{title}</p>
<p className='text-sm leading-relaxed text-current/80'>{message}</p>
</div>
{dismissible ? (
<button onClick={onClose} className='text-current/60 transition hover:text-current'>
<span className='sr-only'>Dismiss alert</span>
ร
</button>
) : null}
</div>
</aside>
);
}
Badge primitive
Micro tag styles for status chips, matching Tailwind tokens across stacks.
import { cn } from './utils/cn';
type BadgeVariant = 'default' | 'primary' | 'success' | 'warning' | 'danger';
const badgeStyles: Record<BadgeVariant, string> = {
default: 'bg-[#F3F4F6] text-[#1b1b18] dark:bg-[#242422] dark:text-[#EDEDEC]',
primary: 'bg-[#E0E7FF] text-[#4338CA]',
success: 'bg-[#DCFCE7] text-[#047857]',
warning: 'bg-[#FEF3C7] text-[#92400E]',
danger: 'bg-[#FEE2E2] text-[#B91C1C]',
};
interface BadgeProps {
variant?: BadgeVariant;
children: React.ReactNode;
}
export function Badge({ variant = 'default', children }: BadgeProps) {
return (
<span
className={cn(
'inline-flex items-center justify-center rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-[0.3em]',
badgeStyles[variant]
)}
>
{children}
</span>
);
}
Navigation tabs
Router-aware tabs using the same rounded pill aesthetic.
import { NavLink } from 'react-router-dom';
import { cn } from './utils/cn';
const tabs = [
{ label: 'Overview', href: '/dashboard', exact: true },
{ label: 'Activity', href: '/dashboard/activity' },
{ label: 'Settings', href: '/dashboard/settings' },
];
export function DashboardTabs() {
return (
<nav className='flex items-center gap-2 rounded-full border border-[#e3e3e0] bg-white p-1 dark:border-[#2a2a29] dark:bg-[#141413]'>
{tabs.map((tab) => (
<NavLink
key={tab.href}
to={tab.href}
end={tab.exact}
className={({ isActive }) =>
cn(
'flex-1 rounded-full px-4 py-2 text-sm font-medium transition md:flex-none',
isActive
? 'bg-[#1b1b18] text-white shadow-sm dark:bg-white dark:text-[#1b1b18]'
: 'text-[#706f6c] hover:bg-[#F3F4F6] dark:text-[#A1A09A] dark:hover:bg-[#1F1F1D]'
)
}
>
{tab.label}
</NavLink>
))}
</nav>
);
}
Data table rowset
Table with soft dividers and status badges, ideal for admin views.
import { Badge } from './Badge';
interface Row {
id: string;
name: string;
email: string;
role: string;
status: 'Active' | 'Invited' | 'Suspended';
}
const rows: Row[] = [
{ id: '1', name: 'Avery Li', email: 'avery@webpros.dev', role: 'Product Lead', status: 'Active' },
{ id: '2', name: 'Noah Vega', email: 'noah@webpros.dev', role: 'Design Ops', status: 'Active' },
{ id: '3', name: 'Mila Reyes', email: 'mila@webpros.dev', role: 'Gameplay Engineer', status: 'Invited' },
];
export function TeamTable() {
return (
<div className='overflow-hidden rounded-2xl border border-[#e3e3e0] dark:border-[#2a2a29]'>
<table className='min-w-full divide-y divide-[#e3e3e0] dark:divide-[#2a2a29]'>
<thead className='bg-[#F9FAFB] text-left text-xs font-semibold uppercase tracking-[0.3em] text-[#706f6c] dark:bg-[#161615] dark:text-[#A1A09A]'>
<tr>
<th className='px-6 py-3'>Name</th>
<th className='px-6 py-3'>Email</th>
<th className='px-6 py-3'>Role</th>
<th className='px-6 py-3'>Status</th>
</tr>
</thead>
<tbody className='divide-y divide-[#e3e3e0]/70 text-sm text-[#41403d] dark:divide-[#2a2a29]/70 dark:text-[#C7C6C2]'>
{rows.map((row) => (
<tr key={row.id} className='hover:bg-[#FDFDFC] dark:hover:bg-[#1b1b18]'>
<td className='px-6 py-4 font-medium text-[#1b1b18] dark:text-[#EDEDEC]'>{row.name}</td>
<td className='px-6 py-4'>{row.email}</td>
<td className='px-6 py-4'>{row.role}</td>
<td className='px-6 py-4'>
<Badge variant={row.status === 'Active' ? 'success' : row.status === 'Invited' ? 'primary' : 'warning'}>
{row.status}
</Badge>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
Modal workflow
Accessible modal with inline form controls and matching spacing scale.
import { useState } from 'react';
interface ModalProps {
title: string;
children: React.ReactNode;
onClose: () => void;
}
function Modal({ title, children, onClose }: ModalProps) {
return (
<div className='fixed inset-0 z-50 flex items-center justify-center bg-black/40 px-4 py-8'>
<div className='w-full max-w-lg rounded-3xl border border-[#e3e3e0] bg-white p-8 shadow-2xl dark:border-[#2a2a29] dark:bg-[#141413]'>
<header className='flex items-start justify-between gap-6'>
<div className='space-y-2'>
<h3 className='text-2xl font-semibold text-[#1b1b18] dark:text-[#EDEDEC]'>{title}</h3>
<p className='text-sm text-[#706f6c] dark:text-[#A1A09A]'>Spin up a project brief in a couple of clicks.</p>
</div>
<button onClick={onClose} className='text-[#706f6c] transition hover:text-[#1b1b18] dark:hover:text-white'>
<span className='sr-only'>Close modal</span>
ร
</button>
</header>
<div className='mt-6 space-y-4 text-sm text-[#41403d] dark:text-[#C7C6C2]'>{children}</div>
<footer className='mt-8 flex flex-col-reverse gap-3 pt-6 md:flex-row md:justify-end'>
<button
onClick={onClose}
className='w-full rounded-full border border-[#e3e3e0] px-5 py-3 text-sm font-semibold text-[#1b1b18] hover:bg-[#F3F4F6] dark:border-[#2a2a29] dark:text-[#EDEDEC] dark:hover:bg-[#1F1F1D] md:w-auto'
>
Cancel
</button>
<button className='w-full rounded-full bg-[#526DFF] px-5 py-3 text-sm font-semibold text-white transition hover:bg-[#3D4EEB] md:w-auto'>
Create project
</button>
</footer>
</div>
</div>
);
}
export function ModalExample() {
const [open, setOpen] = useState(false);
return (
<div>
<button
onClick={() => setOpen(true)}
className='rounded-full bg-[#1b1b18] px-5 py-3 text-sm font-semibold text-white transition hover:bg-[#0f0f0d] dark:bg-white dark:text-[#1b1b18] dark:hover:bg-[#EDEDEC]'
>
Launch modal
</button>
{open ? (
<Modal title='Kickoff project' onClose={() => setOpen(false)}>
<label className='block space-y-2'>
<span className='text-xs font-semibold uppercase tracking-[0.3em] text-[#706f6c] dark:text-[#A1A09A]'>Project name</span>
<input className='w-full rounded-xl border border-[#e3e3e0] px-4 py-3 text-sm focus:border-[#526DFF] focus:outline-none focus:ring-2 focus:ring-[#526DFF]/40 dark:border-[#2a2a29] dark:bg-[#141413]' placeholder='Launchpad redesign' />
</label>
<label className='block space-y-2'>
<span className='text-xs font-semibold uppercase tracking-[0.3em] text-[#706f6c] dark:text-[#A1A09A]'>Timeline</span>
<select className='w-full rounded-xl border border-[#e3e3e0] px-4 py-3 text-sm focus:border-[#526DFF] focus:outline-none focus:ring-2 focus:ring-[#526DFF]/40 dark:border-[#2a2a29] dark:bg-[#141413]'>
<option>2 weeks</option>
<option>4 weeks</option>
<option>6 weeks</option>
</select>
</label>
</Modal>
) : null}
</div>
);
}
Avatar fallback
Initial-based avatar to backfill missing profile images.
import { cn } from './utils/cn';
interface AvatarProps {
name: string;
imageUrl?: string;
size?: 'sm' | 'md' | 'lg';
}
const sizeMap: Record<NonNullable<AvatarProps['size']>, string> = {
sm: 'h-8 w-8 text-xs',
md: 'h-10 w-10 text-sm',
lg: 'h-14 w-14 text-base',
};
export function Avatar({ name, imageUrl, size = 'md' }: AvatarProps) {
if (imageUrl) {
return <img src={imageUrl} alt={name} className={cn('rounded-full object-cover', sizeMap[size])} />;
}
const initials = name
.split(' ')
.map((part) => part[0])
.join('');
return (
<span
className={cn(
'inline-flex items-center justify-center rounded-full bg-[#1b1b18] font-semibold uppercase tracking-[0.2em] text-white dark:bg-[#EDEDEC] dark:text-[#1b1b18]',
sizeMap[size]
)}
>
{initials}
</span>
);
}
3. Storybook + testing
- โก Install
storybook@8for visual parity with the Blade library. - โ
Cover contracts using
vitest+@testing-library/react. - ๐ Publish packages via pnpm workspaces or GitHub Packages for reuse across apps.
Helpful resources
Composition API component kit
Translate the Blade primitives into Vue SFCs with shared tokens, Pinia stores, and Volar DX.
1. Kickstart
npm create vue@latest webproshub-vue -- --typescript
cd webproshub-vue
npm install tailwindcss postcss autoprefixer
npx tailwindcss init -p
Enable Volar + TypeScript then import the shared design tokens into
tailwind.config.ts.
2. Button SFC pattern
<!-- Vue snippet coming soon -->
3. QA + docs
- ๐งช Pair
vitestwith@vue/test-utilsfor interaction coverage. - ๐ Publish stories with
Histoireor Storybook Vue to match Blade previews. - ๐ Bundle as a Nuxt-ready module or standard library using
tsup.