React Components

Systematic index and detailed documentation for the React components provided by @aircada/react for e-commerce 3D product configurations.
Published: 7/10/2026

The @aircada/react library provides specialized, production-ready React components designed to link 2D overlays, custom dropdowns, step builders, and image uploads directly to the 3D Engine canvas and synchronized store state.

These components enable seamless user interactions, automated transitions, and smooth 3D viewport canvas integration.

AirStickerUploader Component#

A plug-and-play sticker upload component that coordinates media uploads, dropzone state, and transform engine layout locks to prevent visual jumps in 3D views.

The AirStickerUploader component manages the entire pipeline of uploading a custom image (decal/sticker) and placing it onto a 3D canvas object. It integrates a 2D dropzone, client-side temporary media upload, and the AirTransformControls component.

The Atomic Layout Lock (No-Jump Pipeline)

Rendering a sticker in 3D before calculating its boundaries causes a momentary visual glitch/jump. AirStickerUploader resolves this with a deferred flush lock:

1. When a file is dropped, the uploader uploads it as a temporary item to window.aircadaverse.primaryRealm.mediaManager.makeTemporaryMediaItemFromBlob and holds the returned ID in pendingUploadIdRef.

2. The component mounts the image inside a hidden/under-evaluation AirTransformControls block.

3. Once the 2D transform engine calculates the correct aspect-ratio boundaries and fires the first change callback, the uploader releases the lock, firing onAtomicUploadReady(mediaId, transformData).

4. The parent application receives both the asset ID and the pristine centered coordinates at the exact same moment, performing a clean, glitch-free mount in the 3D scene.

Props Interface

typescript
export interface AircadaStickerUploaderProps {
  onAtomicUploadReady: (operatorId: string, transformData: TransformData) => void;
  onTransformUpdate: (transformData: TransformData) => void;
  onImagePreviewSelected?: (localUrl: string) => void;
  onClearImage?: () => void;
  className?: string;
  activeClassName?: string;
  dragActiveClassName?: string;
  renderPlaceholder?: (isDragActive: boolean) => React.ReactNode;
  renderControlsOverlay?: (onClear: () => void, doReplace: () => void) => React.ReactNode;
  uploadedImageUrl?: string;
  transformProps?: { x?: number, y?: number, scale?: number, rotation?: number };
  controlsConfig?: Partial<AirTransformControlsProps>;
  flipY?: boolean;
}

Example Integration Pattern

Use AirStickerUploader within a sticker editor panel to update a sticker effect on a 3D model:

tsx
import React, { useState } from "react";
import { AirStickerUploader, TransformData } from "@aircada/react";
import { useAirStore } from "@aircada/react";

export function StickerCustomizer() {
  const [store, patch] = useAirStore();
  const activeSticker = store.product.sticker;

  const handleAtomicUpload = (mediaId: string, transform: TransformData) => {
    // Commit the temporary media ID and initial clean transform to the store
    patch({
      product: {
        sticker: {
          assetId: mediaId,
          x: transform.x,
          y: transform.y,
          scale: transform.scale,
          rotation: transform.rotation
        }
      }
    });
  };

  const handleTransformUpdate = (transform: TransformData) => {
    // Continuously update transform parameters in the store as user drags
    patch({
      product: {
        sticker: {
          x: transform.x,
          y: transform.y,
          scale: transform.scale,
          rotation: transform.rotation
        }
      }
    });
  };

  return (
    <div className="p-4 bg-gray-900 rounded-2xl w-80 pointer-events-auto">
      <h3 className="text-white text-md font-semibold mb-3">Upload Artwork</h3>
      <AirStickerUploader
        onAtomicUploadReady={handleAtomicUpload}
        onTransformUpdate={handleTransformUpdate}
        uploadedImageUrl={activeSticker?.assetId}
        transformProps={{
          x: activeSticker?.x,
          y: activeSticker?.y,
          scale: activeSticker?.scale,
          rotation: activeSticker?.rotation
        }}
        className="h-32 border-2 border-dashed border-gray-700 rounded-xl flex items-center justify-center"
        activeClassName="border-solid border-blue-500"
      />
    </div>
  );
}

AirTransformControls Component#

An interactive 2D translation, scale, and rotation overlay wrapper that maps user gestures to exact coordinate updates.

The AirTransformControls component is an overlay system that wraps any child element (typically an image/sticker preview) in a bounding box equipped with corner drag handles. It tracks drag (translation), corner drags (scaling), and rotational gestures to output precise coordinate offsets.

Key Capabilities

1. Dimension Observability: Uses a ResizeObserver internally to defer initialization until the target element has nonzero physical dimensions. This prevents premature layout calculations and avoids the common 0x0 cache trap.

2. State Persistence: Accepts a persistenceKey prop. When provided, the component automatically saves transform states to localStorage under the key air-transform-${persistenceKey} and restores them on mount, overriding autoCenterOnMount.

3. Mock Input Manager: Instantiates a custom MockInputManager bridge to parse mouse and touch gestures identically across devices.

4. Aspect Fit Modes: Supports auto-fitting (autoFitMode: 'fitWidth' | 'fitHeight' | 'contain' | 'cover') to scale the source element relative to container bounds.

Suggested Use Pattern

Wrap a sticker thumbnail inside AirTransformControls and update state variables or stores inside onChange:

AirTransformControls.types.ts (typescript)
export interface AirTransformControlsProps {
  children: ReactNode;
  x?: number;
  y?: number;
  scale?: number;
  rotation?: number;
  defaultRotation?: number;
  onChange?: (data: TransformData) => void;
  bounds?: any;
  scrollContainerMode?: 'parent' | 'blockContainer' | string;
  allowOverscale?: boolean;
  resizeFromCenter?: boolean;
  minScale?: number;
  maxScale?: number;
  persistenceKey?: string;
  autoCenterOnMount?: boolean;
  draggable?: boolean;
  resizable?: boolean;
  rotatable?: boolean;
  flipX?: boolean;
  flipY?: boolean;
  customHandles?: ReactNode;
}
Props interface definition for AirTransformControlsProps.
ArtworkManipulator.tsx (tsx)
import React, { useState } from "react";
import { AirTransformControls, TransformData } from "@aircada/react";

export function ArtworkManipulator() {
  const [transform, setTransform] = useState<TransformData>({
    x: 0,
    y: 0,
    scale: 1,
    rotation: 0
  });

  return (
    <div className="w-[500px] h-[500px] border border-gray-800 relative bg-black/40 rounded-3xl overflow-hidden">
      <AirTransformControls
        x={transform.x}
        y={transform.y}
        scale={transform.scale}
        rotation={transform.rotation}
        onChange={(data) => setTransform(prev => ({ ...prev, ...data }))}
        persistenceKey="sticker-editor-main"
        autoCenterOnMount={true}
        minScale={0.2}
        maxScale={3.0}
      >
        <img
          src="/assets/sticker-graphic.png"
          alt="Editable Graphic"
          className="max-h-[150px] object-contain pointer-events-none"
        />
      </AirTransformControls>
    </div>
  );
}
Implementing AirTransformControls with local state tracking and scale limits.

AircadaViewport Component#

The core viewport component that embeds and manages the 3D WebGL canvas frame within a React application hierarchy.

The AircadaViewport component is a fundamental building block of any Aircada overlay interface. It serves as the physical mount point for the 3D canvas. When mounted, it instructs the underlying client engine to append the active WebGL viewer canvas container directly inside its DOM node.

Core Responsibilities

1. Engine Hijacking: Intercepts the engine's CanvasFrameController and attaches it to the component's internal ref container.

2. Cleanup & Prevention: Tracks the original parent node so that when React triggers a layout reflow or unmounts the component, the WebGL context is safely returned to the body rather than being destroyed.

3. Gesture Blocking: Automatically applies the CSS property touch-action: none to the DOM wrapper. This is non-negotiable for preventing default mobile swipe-to-scroll behavior from breaking orbit camera rotation.

Suggested Use Pattern

Place AircadaViewport as an absolute-positioned container filling the screen beneath your interactive 2D pointer overlays:

AircadaViewport.types.ts (typescript)
export interface AircadaViewportProps {
  className?: string;
  style?: React.CSSProperties;
}
Props interface definition for AircadaViewport.
ConfiguratorLayout.tsx (tsx)
import React from "react";
import { AircadaViewport } from "@aircada/react";

export function ConfiguratorLayout() {
  return (
    <div className="relative w-full h-screen overflow-hidden pointer-events-none">
      {/* 3D Scene Viewport */}
      <AircadaViewport className="absolute inset-0 z-0" />

      {/* 2D Interactive UI Elements overlayed on top */}
      <div className="absolute bottom-6 left-6 z-10 pointer-events-auto bg-black/60 p-4 rounded-xl">
        <button className="text-white px-4 py-2 bg-blue-500 rounded">
          Reset Camera
        </button>
      </div>
    </div>
  );
}
Embedding AircadaViewport with overlayed interactive 2D control buttons.

HorizontalScrollContainer Component#

A pure structural container that implements click-and-drag panning, hidden scrollbars, vertical bleed boundaries, and auto-centering based on active child IDs.

The HorizontalScrollContainer is a structural React wrapper that converts static lists of interactive buttons, models, or configurations into a smooth horizontal carousel.

Key Capabilities

1. Click-and-Drag Panning: Enables mouse-drag scrolling with perfect 1:1 pointer tracking. It automatically sets a hasDragged threshold (5px) and intercept clicks via onClickCapture so that dragging the list does not accidentally trigger button clicks on child elements.

2. Auto-Centering: Observes the activeId prop. When it changes, the container queries its DOM tree for an element containing data-id="{activeId}", calculates its horizontal offset relative to the viewport, and calls scrollTo({ left: targetScroll, behavior: 'smooth' }) to center the item.

3. Vertical Bleed Overrides: Scrolling elements usually require overflow-x: auto which clips vertical overflow. By using the verticalBleed prop, the container applies a padding offset and equivalent negative margins, allowing visual items (like tooltips, drop shadows, or scaling hover effects) to overflow top and bottom borders without clipping.

Suggested Use Pattern

Wrap configurator cards inside the scroll container, providing a matching data-id to the active option item:

HorizontalScrollContainer.types.ts (typescript)
export interface HorizontalScrollContainerProps {
  children: React.ReactNode;
  activeId?: string | number;
  className?: string;
  innerClassName?: string;
  gap?: number;
  verticalBleed?: number;
  justify?: 'start' | 'center' | 'end' | 'between' | 'evenly' | 'around';
}
Props interface definition for HorizontalScrollContainer.
MaterialSelector.tsx (tsx)
import React, { useState } from "react";
import { HorizontalScrollContainer } from "@aircada/react";

const options = [
  { id: "opt-1", label: "Midnight Black" },
  { id: "opt-2", label: "Polished Steel" },
  { id: "opt-3", label: "Crimson Red" },
  { id: "opt-4", label: "Emerald Green" }
];

export function MaterialSelector() {
  const [selectedId, setSelectedId] = useState("opt-1");

  return (
    <div className="w-full bg-gray-950 p-6 pointer-events-auto">
      <HorizontalScrollContainer
        activeId={selectedId}
        gap={12}
        verticalBleed={20}
        justify="center"
      >
        {options.map(opt => (
          <button
            key={opt.id}
            data-id={opt.id}
            onClick={() => setSelectedId(opt.id)}
            className={`px-6 py-4 rounded-xl flex-shrink-0 transition-transform duration-200 hover:scale-105
              ${selectedId === opt.id ? "bg-cyan-500 text-black font-semibold" : "bg-gray-800 text-white"}
            `}
          >
            {opt.label}
          </button>
        ))}
      </HorizontalScrollContainer>
    </div>
  );
}
Implementing HorizontalScrollContainer with active item binding and styling.

StepPanel Component#

A structural layout and slide-transition manager for multi-step product configurators, providing header slots and custom navigation button controls.

The StepPanel component is a layout manager specifically engineered for sequential workflows, such as step-by-step 3D product customization. It manages mounting steps, triggering transition CSS keyframes, and handling header layouts.

Key Capabilities

1. Slide Transition Engine: Uses a local hook (useStepTransitions) to queue active steps. When activeStepIndex changes, the hook temporarily preserves the exiting step and mounts the entering step simultaneously. It injects a style block containing @keyframes definitions (step-enter-right, step-exit-left, etc.) to execute a 500ms sliding animation.

2. Content Isolation: Isolates the sliding body content from header logic, allowing users to scroll page content while keeping step titles and chevrons fixed at the top.

3. Extensible Header Slots: To avoid hardcoding application-specific features (such as menus, checkout buttons, or minimize buttons), the component exposes clean React Node slots: leftHeaderContent and rightHeaderContent.

Suggested Use Pattern

Initialize steps containing distinct configuration parameters, linking activeStepIndex to a local state or store step parameter:

StepPanel.types.ts (typescript)
export interface StepDefinition {
  id: string;
  name: string;
  content: React.ReactNode;
}

export interface StepPanelProps {
  steps: StepDefinition[];
  activeStepIndex: number;
  onNext: () => void;
  onPrev: () => void;
  className?: string;
  heightClass?: string;
  renderHeaderTitle?: (props: { step: StepDefinition; index: number; totalSteps: number }) => React.ReactNode;
  renderNextButton?: (props: { onNext: () => void; disabled: boolean }) => React.ReactNode;
  renderPrevButton?: (props: { onPrev: () => void; disabled: boolean }) => React.ReactNode;
  leftHeaderContent?: React.ReactNode;
  rightHeaderContent?: React.ReactNode;
}
StepPanel components props and StepDefinition interfaces.
CustomizerSteps.tsx (tsx)
import React, { useState } from "react";
import { StepPanel, StepDefinition } from "@aircada/react";

export function CustomizerSteps() {
  const [activeIndex, setActiveIndex] = useState(0);

  const steps: StepDefinition[] = [
    { id: "step-base", name: "Base Material", content: <div>Base Materials list...</div> },
    { id: "step-laces", name: "Lace Design", content: <div>Lace styles picker...</div> },
    { id: "step-sticker", name: "Custom Graphic", content: <div>Sticker Uploader panel...</div> }
  ];

  return (
    <div className="absolute inset-x-0 bottom-0 pointer-events-none">
      <StepPanel
        steps={steps}
        activeStepIndex={activeIndex}
        onNext={() => setActiveIndex(i => Math.min(i + 1, steps.length - 1))}
        onPrev={() => setActiveIndex(i => Math.max(i - 1, 0))}
        className="pointer-events-auto rounded-t-[32px] bg-slate-900 border-t border-slate-800 text-white"
        heightClass="h-[400px]"
        leftHeaderContent={
          <button onClick={() => console.log("Minimize clicked")} className="text-gray-400 hover:text-white">
            Minimize
          </button>
        }
      />
    </div>
  );
}
Implementing StepPanel with minimize buttons and state management.