Logo
  1. Docs
  2. Development Guide

Creating Components

Published on Aug 15, 2025, updated 4 days ago

Creating Components

This guide walks you through creating Weaverse components from scratch, covering everything from basic component structure to advanced patterns with TypeScript and React 19.

Table of Contents

Component Structure

Every Weaverse component follows this structure:

app/sections/my-component/├── index.tsx           # Main component file├── schema.ts          # Schema definition (optional separate file)└── loader.ts          # Data loader (optional separate file)

Or as a single file:

app/sections/my-component.tsx

Required Exports

Each component file must export:

  1. Default export - The React component
  2. schema export - Component configuration
  3. loader export (optional) - Server-side data loading

Basic Component Example

Here's a simple component following modern React 19 patterns:

// app/sections/hero-banner/index.tsximport { createSchema } from "@weaverse/hydrogen";import type { HydrogenComponentProps } from "@weaverse/hydrogen";
interface HeroBannerProps extends HydrogenComponentProps {  heading: string;  description: string;  buttonText: string;  buttonLink: string;  backgroundImage: string;}
function HeroBanner(props: HeroBannerProps) {  const {     heading,     description,     buttonText,     buttonLink,     backgroundImage,    children,    ...rest   } = props;
  return (    <section       {...rest}      className="relative min-h-[500px] flex items-center justify-center bg-cover bg-center"      style={{ backgroundImage: `url(${backgroundImage})` }}    >      <div className="absolute inset-0 bg-black/40" />      <div className="relative z-10 text-center text-white max-w-4xl mx-auto px-4">        <h1 className="text-4xl md:text-6xl font-bold mb-4">{heading}</h1>        <p className="text-lg md:text-xl mb-8">{description}</p>        {buttonText && buttonLink && (          <a             href={buttonLink}            className="inline-block bg-white text-black px-8 py-3 rounded-lg font-semibold hover:bg-gray-100 transition-colors"          >            {buttonText}          </a>        )}        {children}      </div>    </section>  );}
export default HeroBanner;
export const schema = createSchema({  type: "hero-banner",  title: "Hero Banner",  settings: [    {      group: "Content",      inputs: [        {          type: "text",          name: "heading",          label: "Heading",          defaultValue: "Welcome to Our Store",          placeholder: "Enter heading text",        },        {          type: "textarea",          name: "description",          label: "Description",           defaultValue: "Discover amazing products and exceptional service.",          placeholder: "Enter description text",        },        {          type: "text",          name: "buttonText",          label: "Button Text",          defaultValue: "Shop Now",        },        {          type: "url",          name: "buttonLink",          label: "Button Link",          defaultValue: "/collections/all",        },      ],    },    {      group: "Design",      inputs: [        {          type: "image",          name: "backgroundImage",          label: "Background Image",        },      ],    },  ],  childTypes: ["subheading", "paragraph"],  presets: {    heading: "Welcome to Our Store",    description: "Discover amazing products and exceptional service.",    buttonText: "Shop Now",    buttonLink: "/collections/all",    children: [      {        type: "subheading",        content: "Limited Time Offer",      },    ],  },});

Component Registration

IMPORTANT: After creating your component, you must register it in the components file to make it available in Weaverse Studio.

Registration Steps

  1. Import your component in the components file:
// app/weaverse/components.ts (or templates/pilot/app/weaverse/components.ts)import type { HydrogenComponent } from "@weaverse/hydrogen";
// Import your new componentimport * as HeroBanner from "~/sections/hero-banner";import * as ExistingComponent from "~/sections/existing-component";// ... other imports
export const components: HydrogenComponent[] = [  // Add your component to the array  HeroBanner,  ExistingComponent,  // ... other components];
  1. Restart your development server to see the component in Weaverse Studio:
npm run dev

Registration Example from Pilot Template

Here's how components are registered in the official Pilot template:

// templates/pilot/app/weaverse/components.tsimport * as HeroImage from "~/sections/hero-image";import * as FeaturedProducts from "~/sections/featured-products";  import * as ImageWithText from "~/sections/image-with-text";// ... many more imports
export const components: HydrogenComponent[] = [  HeroImage,  FeaturedProducts,  ImageWithText,  // ... 100+ registered components];

Common Registration Issues

Problem: Component doesn't appear in Weaverse Studio after creation

# ❌ Component created but not registered

Solution: Always register new components in the components array

// ✅ Add to components.ts and restart dev serverimport * as MyNewComponent from "~/sections/my-new-component";
export const components: HydrogenComponent[] = [  MyNewComponent, // Add this line  // ... existing components];

Problem: Import path errors

# ❌ Wrong: Using default imports  import MyComponent from "~/sections/my-component";

Solution: Always use namespace imports with * as

// ✅ Correct: Using namespace importimport * as MyComponent from "~/sections/my-component";

Schema Definition

The schema defines how your component appears and behaves in Weaverse Studio:

Basic Schema Structure

export const schema = createSchema({  type: "component-name",        // Unique identifier  title: "Component Title",      // Display name in Studio  settings: [                    // Configuration groups    {      group: "Content",           // Group name      inputs: [                   // Input controls        {          type: "text",           // Input type          name: "propertyName",   // Property name          label: "Display Label", // Label in Studio          defaultValue: "Default", // Default value        },      ],    },  ],  childTypes: ["heading", "paragraph"], // Allowed child components  presets: {                     // Default values and children    propertyName: "value",    children: [      { type: "heading", content: "Sample Heading" },    ],  },});

Input Types

Weaverse supports various input types:

settings: [  {    group: "Content",    inputs: [      { type: "text", name: "title", label: "Title" },      { type: "textarea", name: "description", label: "Description" },      { type: "richtext", name: "content", label: "Rich Content" },      { type: "url", name: "link", label: "Link URL" },      { type: "image", name: "image", label: "Image" },      { type: "video", name: "video", label: "Video" },      { type: "color", name: "color", label: "Color" },      { type: "range", name: "opacity", label: "Opacity", configs: { min: 0, max: 100 } },      { type: "toggle", name: "enabled", label: "Enable Feature" },      { type: "select", name: "layout", label: "Layout", configs: {        options: [          { value: "grid", label: "Grid" },          { value: "list", label: "List" },        ],      }},    ],  },]

TypeScript Integration

Component Props Interface

Always define proper TypeScript interfaces:

import type { HydrogenComponentProps } from "@weaverse/hydrogen";
interface MyComponentProps extends HydrogenComponentProps {  // Define your component-specific props  title: string;  description?: string;  layout: "grid" | "list";  enabled: boolean;  opacity: number;}
function MyComponent(props: MyComponentProps) {  // Component implementation with full type safety}

Schema Type Safety

You can extract types from your schema:

import type { InferInput } from "@weaverse/hydrogen";
export const schema = createSchema({  // ... schema definition});
// Extract the props type from schematype MyComponentProps = InferInput<typeof schema> & HydrogenComponentProps;
function MyComponent(props: MyComponentProps) {  // Fully typed component}

Data Loading

For components that need server-side data, export a loader function:

import type { ComponentLoaderArgs } from "@weaverse/hydrogen";
interface FeaturedProductData {  productHandle: string;}
export const loader = async (args: ComponentLoaderArgs<FeaturedProductData>) => {  const { weaverse, data } = args;  const { storefront } = weaverse;    if (!data?.productHandle) {    return null;  }    const { product } = await storefront.query(PRODUCT_QUERY, {    variables: { handle: data.productHandle },  });
  return { product };};
type FeaturedProductProps = HydrogenComponentProps<  Awaited<ReturnType<typeof loader>>> & FeaturedProductData;
function FeaturedProduct(props: FeaturedProductProps) {  const { loaderData, productHandle } = props;  const product = loaderData?.product;    if (!product) {    return <div>Select a product in the Studio</div>;  }    return (    <div>      <h2>{product.title}</h2>      <p>{product.description}</p>      {/* Render product details */}    </div>  );}

Styling Patterns

Tailwind CSS Integration

Use Tailwind classes for styling:

function ProductCard(props: ProductCardProps) {  const { product, layout } = props;    return (    <div className={cn(      "group relative overflow-hidden rounded-lg bg-white shadow-sm transition-shadow hover:shadow-lg",      layout === "compact" && "max-w-sm",      layout === "wide" && "max-w-2xl"    )}>      <div className="aspect-square overflow-hidden">        <img           src={product.image}           alt={product.title}          className="h-full w-full object-cover transition-transform group-hover:scale-105"        />      </div>      <div className="p-4">        <h3 className="font-semibold text-gray-900">{product.title}</h3>        <p className="mt-1 text-sm text-gray-600">{product.price}</p>      </div>    </div>  );}

Class Variance Authority (CVA)

For complex component variants:

import { cva } from "class-variance-authority";
const buttonVariants = cva(  "inline-flex items-center justify-center rounded-md font-medium transition-colors",  {    variants: {      variant: {        primary: "bg-blue-600 text-white hover:bg-blue-700",        secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300",        outline: "border border-gray-300 bg-transparent hover:bg-gray-50",      },      size: {        sm: "h-8 px-3 text-sm",        md: "h-10 px-4",        lg: "h-12 px-6 text-lg",      },    },    defaultVariants: {      variant: "primary",      size: "md",    },  });
function Button(props: ButtonProps) {  const { variant, size, children, ...rest } = props;    return (    <button       className={buttonVariants({ variant, size })}      {...rest}    >      {children}    </button>  );}

Best Practices

1. Component Organization

// ✅ Good: Clear structure and separation of concernsfunction ProductGrid(props: ProductGridProps) {  const { products, columns, spacing } = props;    if (!products?.length) {    return <EmptyState message="No products found" />;  }    return (    <div className={getGridClasses(columns, spacing)}>      {products.map((product) => (        <ProductCard key={product.id} product={product} />      ))}    </div>  );}
// Helper function for grid classesfunction getGridClasses(columns: number, spacing: string) {  return cn(    "grid",    columns === 2 && "grid-cols-2",    columns === 3 && "grid-cols-3",     columns === 4 && "grid-cols-4",    spacing === "tight" && "gap-2",    spacing === "normal" && "gap-4",    spacing === "loose" && "gap-8"  );}

2. Error Boundaries

import { ErrorBoundary } from "~/components/ErrorBoundary";
function DataComponent(props: DataComponentProps) {  return (    <ErrorBoundary fallback={<div>Failed to load data</div>}>      <DataDisplay {...props} />    </ErrorBoundary>  );}

3. Loading States

function ProductList(props: ProductListProps) {  const { loaderData } = props;    if (!loaderData) {    return <ProductListSkeleton />;  }    const { products } = loaderData;    return (    <div>      {products.map(product => (        <ProductCard key={product.id} product={product} />      ))}    </div>  );}

4. Responsive Design

function ResponsiveGrid(props: ResponsiveGridProps) {  return (    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">      {props.children}    </div>  );}

Advanced Patterns

Context Providers

import { createContext, useContext } from "react";
const ProductContext = createContext<Product | null>(null);
function ProductProvider({ product, children }: ProductProviderProps) {  return (    <ProductContext.Provider value={product}>      {children}    </ProductContext.Provider>  );}
function useProduct() {  const product = useContext(ProductContext);  if (!product) {    throw new Error("useProduct must be used within ProductProvider");  }  return product;}

Compound Components

function Card(props: CardProps) {  return (    <div className="rounded-lg border bg-white shadow-sm">      {props.children}    </div>  );}
function CardHeader(props: CardHeaderProps) {  return (    <div className="border-b p-4">      {props.children}    </div>  );}
function CardContent(props: CardContentProps) {  return (    <div className="p-4">      {props.children}    </div>  );}
// Export as compound componentCard.Header = CardHeader;Card.Content = CardContent;
export default Card;

Schema Composition

import { baseLayoutInputs, spacingInputs } from "~/lib/schema-helpers";
export const schema = createSchema({  type: "complex-component",  title: "Complex Component",  settings: [    {      group: "Content",      inputs: [        { type: "text", name: "title", label: "Title" },        { type: "textarea", name: "description", label: "Description" },      ],    },    {      group: "Layout",      inputs: baseLayoutInputs,    },    {      group: "Spacing",      inputs: spacingInputs,    },  ],});

Next Steps

For more examples, check out the Example Components and join our Slack community for help.