3D Cards in Webflow Using Three.js and GLB Models


3dcardswebflow

I’ve always been interested in finding simple ways to bring more depth into web interfaces, not just through visuals, but through interaction and space.

In this demo, I explored how flat UI cards can become interactive 3D scenes using GLB models, Three.js, and Webflow. Each card starts as a basic layout but reveals a small, self-contained environment built with real-time rendering and subtle motion.

It’s a lightweight approach to adding spatial storytelling to familiar components, using tools many designers already work with.

Table of Contents

Welcome to My Creative World

I’m always drawn to visuals that mix the futuristic with the familiar — space-inspired forms, minimal layouts, and everyday elements seen from a different angle.

Most of my projects start this way: by reimagining ordinary ideas through a more immersive or atmospheric lens.

It All Started with a Moodboard

This one began with a simple inspiration board:

Frame 6 4

From that board, I picked a few of my favorite visuals and ran them through an AI tool that converts images into GLB 3D models.

The results were surprisingly good! Abstract, textured, and full of character.

MacBook Pro 14 2

The Concept: Flat to Deep

When I saw the output from the AI-generated GLB models, I started thinking about how we perceive depth in UI design, not just visually, but interactively.

That led to a simple idea: what if flat cards could reveal a hidden spatial layer? Not through animation alone, but through actual 3D geometry, lighting, and camera movement.

I designed three UI cards, each styled with minimal HTML and CSS in Webflow. On interaction, they load a unique GLB model into a Three.js scene directly within the card container. Each model is lit, framed, and animated to create the feeling of a self-contained 3D space.

Image 5 21 25 at 6.08 PM

Building the Web Experience

The layout was built in Webflow using a simple flexbox structure with three cards inside a wrapper. Each card contains a div that serves as the mounting point for a 3D object.

The GLB models are rendered using Three.js, which is integrated into the project with custom JavaScript. Each scene is initialized and handled separately, giving each card its own interactive 3D space while keeping the layout lightweight and modular.

Image 5 21 25 at 6.11 PM

Scene Design with Blender

Each GLB model was prepared in Blender, where I added a surrounding sphere to create a sense of depth and atmosphere. This simple shape helps simulate background contrast and encloses the object in a self-contained space.

Lighting played an important role; especially with reflective materials like glass or metal. Highlights and soft shadows were used to create that subtle, futuristic glow.

The result is that each 3D model feels like it lives inside its own ambient environment, even when rendered in a small card.

Image 5 21 25 at 6.03 PM

Bringing It Together with Three.js

Once the models were exported from Blender as .glb files, I used Three.js to render them inside each card. Each card container acts as its own 3D scene, initialized through a custom JavaScript function.

The setup involves creating a basic scene with a perspective camera, ambient and directional lighting, and a WebGL renderer. I used GLTFLoader to load each .glb file and OrbitControls to enable subtle rotation. Zooming and panning are disabled to keep the interaction focused and controlled.

Each model is loaded into a separate container, making it modular and easy to manage. The camera is offset slightly for a more dynamic starting view, and the background is kept dark to help the lighting pop.

Here’s the full JavaScript used to load and render the models:

// Import required libraries
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import gsap from 'gsap';

/**
 * This function initializes a Three.js scene inside a given container
 * and loads a .glb model into it.
 */
function createScene(containerSelector, glbPath) {
  const container = document.querySelector(containerSelector);

  // 1. Create a scene
  const scene = new THREE.Scene();
  scene.background = new THREE.Color(0x202020); // dark background

  // 2. Set up the camera with perspective
  const camera = new THREE.PerspectiveCamera(
    45, // Field of view
    container.clientWidth / container.clientHeight, // Aspect ratio
    0.1, // Near clipping plane
    100  // Far clipping plane
  );
  camera.position.set(2, 0, 0); // Offset to the side for better viewing

  // 3. Create a renderer and append it to the container
  const renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(container.clientWidth, container.clientHeight);
  container.appendChild(renderer.domElement);

  // 4. Add lighting
  const light = new THREE.DirectionalLight(0xffffff, 4);
  light.position.set(30, -10, 20);
  scene.add(light);

  const ambientLight = new THREE.AmbientLight(0x404040); // soft light
  scene.add(ambientLight);

  // 5. Set up OrbitControls to allow rotation
  const controls = new OrbitControls(camera, renderer.domElement);
  controls.enableZoom = false; // no zooming
  controls.enablePan = false;  // no dragging
  controls.minPolarAngle = Math.PI / 2; // lock vertical angle
  controls.maxPolarAngle = Math.PI / 2;
  controls.enableDamping = true; // smooth movement

  // 6. Load the GLB model
  const loader = new GLTFLoader();
  loader.load(
    glbPath,
    (gltf) => {
      scene.add(gltf.scene); // Add model to the scene
    },
    (xhr) => {
      console.log(`${containerSelector}: ${(xhr.loaded / xhr.total) * 100}% loaded`);
    },
    (error) => {
      console.error(`Error loading ${glbPath}`, error);
    }
  );

  // 7. Make it responsive
  window.addEventListener("resize", () => {
    camera.aspect = container.clientWidth / container.clientHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(container.clientWidth, container.clientHeight);
  });

  // 8. Animate the scene
  function animate() {
    requestAnimationFrame(animate);
    controls.update(); // updates rotation smoothly
    renderer.render(scene, camera);
  }

  animate(); // start the animation loop
}

// 9. Initialize scenes for each card (replace with your URLs)
createScene(".div",  "https://yourdomain.com/models/yourmodel.glb");
createScene(".div2", "https://yourdomain.com/models/yourmodel2.glb");
createScene(".div3", "https://yourdomain.com/models/yourmodel3.glb");

This script is added via a