Open-source blades

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.

Buttons

Primary CTAs and supporting button styles.

Button variants

Core button styles for primary, secondary, stateful, and outline actions.

Button sizes

Consistent sizing for compact and expressive CTAs.

Buttons as links

Use buttons to highlight navigation to key pages.

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.

Additional actions go here.

Forms

Composable inputs with validation states and helpers.

Stacked form

A complete contact form built with Blade components.

Gender

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.

Default Primary Success Warning Danger

Badge sizes

Adjust sizing to match surrounding UI.

Small Medium Large

Tables

Responsive data tables with optional striping and hover states.

Basic table

A clean table with subtle dividers.

Name Email 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.

JD
JS
AB

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.

Content above
Content below
OR
More content
React + TypeScript

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@8 for 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

Vue 3 + Vite

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 vitest with @vue/test-utils for interaction coverage.
  • ๐Ÿ“š Publish stories with Histoire or Storybook Vue to match Blade previews.
  • ๐Ÿš€ Bundle as a Nuxt-ready module or standard library using tsup.

Helpful resources