Custom website
Brand-first marketing sites and portfolios that don’t look like templates.
- ▸ Landing + CMS-ready pages
- ▸ Mobile-first, fast load
- ▸ SEO-ready structure
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
Switch between Blade-ready snippets and playbooks for React TypeScript or Vue 3 composition setups.
Flexible content containers with optional header and footer regions.
Composable container with just the essentials.
This is a basic card with text content.
Add supporting context without clutter.
Add context right up front.
Use the subtitle prop for extra detail.
Footers are perfect for actions and summaries.
Footers are perfect for buttons or secondary text.
Composable inputs with validation states and helpers.
A complete contact form built with Blade components.
Show inline errors with a single prop.
This email is already taken
Guide users with lightweight helper copy.
Choose something unique and memorable.
Status banners with optional dismiss controls.
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.
Quick labels for statuses, tags, and counts.
Easily differentiate statuses or categories.
Adjust sizing to match surrounding UI.
Responsive data tables with optional striping and hover states.
A clean table with subtle dividers.
| Name | Role | Status | |
|---|---|---|---|
| John Doe | john@example.com | Admin | Active |
| Jane Smith | jane@example.com | User | Active |
Improve readability with zebra striping and hover feedback.
| Product | Price | Stock |
|---|---|---|
| Laptop | $999 | 15 |
| Mouse | $29 | 42 |
Responsive modals in three convenient sizes.
Launch modals of varying sizes with one set of triggers.
Avatars, loaders, empty states, and dividers.
Display user initials or uploaded photos.
Accessible loading indicators.
Friendly placeholder when lists are empty.
Get started by creating a new item.
Break up content with horizontal or labeled dividers.
Scaffold a typed component library that mirrors the Blade implementations—same tokens, same UX specs, lighter SPA footprint.
// 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.
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.
Create a new file called src/index.css
@import "tailwindcss";
Import Tailwind CSS into the src/index.css file.
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.
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-[#22D3EE] text-white hover:bg-[#0891B2]',
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} />
);
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>
);
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-[#22D3EE]'>{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-[#22D3EE]'>{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-[#22D3EE]'>{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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
storybook@8 for visual parity with the Blade
library.vitest +
@testing-library/react.Translate the Blade primitives into Vue SFCs with shared tokens, Pinia stores, and Volar DX.
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.
<!-- Vue snippet coming soon -->
vitest with
@vue/test-utils for interaction coverage.Histoire or Storybook Vue to match
Blade previews.tsup.
Open for builds · Studio slots this month
WebProsHub builds products end-to-end — from branded sites to full platforms. Same studio behind MiMiKu, EasyBooking, and the apps in our catalog.
Brand-first marketing sites and portfolios that don’t look like templates.
Booking, portals, dashboards, SaaS — the kind of product we already run live.
Long-term partnership — product studio energy, not one-off freelancing.