React Three Fiber and drei

React Three Fiber 32,576 (R3F, github.com/pmndrs/react-three-fiber (https://github.com/pmndrs/react-three-fiber 32,576 ), MIT) is a React 7,897 renderer whose JSX elements become Three.js 30,815 objects: <mesh> is new THREE.Mesh(), args are constructor arguments and props set properties. Version 9 needs React 19 (MERN Stack Development teaches React). drei 9,900 (github.com/pmndrs/drei (https://github.com/pmndrs/drei 9,900 ), MIT) wraps add-ons and helpers as components:

App.jsx: BookNest's six books in React Three Fiber and dreiJSX
import { createRoot } from 'react-dom/client';
import { Canvas } from '@react-three/fiber';
import { RoundedBox } from '@react-three/drei';
import { BOOKS } from './bookshelf.js';
createRoot(document.getElementById('root')).render(
  <Canvas camera={{ position: [0.7, 0.9, 2], fov: 35 }}>
    <hemisphereLight args={['#ffffff', '#8d6e63', 1.2]} />
    <directionalLight position={[2, 3, 4]} intensity={2.5} />
    {BOOKS.map((book, i) => (
      <RoundedBox key={book.title} args={[0.14, 0.9, 0.62]} radius={0.02}
        position={[(i - 2.5) * 0.18, 0.45, 0]}
        onClick={(event) => { event.stopPropagation(); console.log('clicked', book.title); }}>
        <meshStandardMaterial color={book.color} />
      </RoundedBox>
    ))}
  </Canvas>
);

Bundled with npx esbuild 126 App.jsx --bundle --jsx=automatic --format=esm --minify and clicked at its center in headless Chrome 1 , it logged clicked Small Steps to Big Summits; without stopPropagation() it also logged Salt and Saffron, because R3F events reach every object the ray hits. R3F 9.8.1 also warned that r186 deprecates THREE.Clock (setAnimationLoop), and it enables ACES Filmic tone mapping unless you write <Canvas flat>.

React Three Fiber: the six books as React components, with hover state and click eventsHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f5f1e8; font: 13px system-ui, sans-serif; color: #263238; }
  #root { width: 600px; max-width: 100%; height: 280px; }
  p { margin: 4px 8px; }
</style>
<!-- The CDN's +esm builds pin slightly different React versions; map them all to one copy -->
<script type="importmap">
{ "imports": {
  "react": "https://cdn.jsdelivr.net/npm/react@19.3.0/+esm",
  "react-dom/client": "https://cdn.jsdelivr.net/npm/react-dom@19.3.0/client/+esm",
  "@react-three/fiber": "https://cdn.jsdelivr.net/npm/@react-three/fiber@9.8.1/+esm",
  "https://cdn.jsdelivr.net/npm/react@19.0.0/+esm": "https://cdn.jsdelivr.net/npm/react@19.3.0/+esm",
  "https://cdn.jsdelivr.net/npm/react@19.2.0/+esm": "https://cdn.jsdelivr.net/npm/react@19.3.0/+esm",
  "https://cdn.jsdelivr.net/npm/react@19.2.8/+esm": "https://cdn.jsdelivr.net/npm/react@19.3.0/+esm",
  "https://cdn.jsdelivr.net/npm/react@%5E19.3.0/+esm": "https://cdn.jsdelivr.net/npm/react@19.3.0/+esm" } }
</script>
<div id="root"></div>
<p id="out">Hover and click a book.</p>
<script type="module">
import { createElement as h, useState, useRef } from 'react';
import { createRoot } from 'react-dom/client';
import { Canvas, useFrame } from '@react-three/fiber';

const BOOKS = [['The Quiet Harbor', '#1f5f8b'], ['Patterns of the Deep Web', '#5b3f99'], ['Salt and Saffron', '#e09a10'],
  ['Small Steps to Big Summits', '#3f7d3a'], ["The Clockmaker's Paradox", '#b5452f'], ['Gardens in Glass', '#2a9d8f']];

// With a bundler this would be JSX: <mesh position={...} onClick={...}><boxGeometry args={...} /></mesh>
// <mesh> is new THREE.Mesh(), args are constructor arguments, other props set properties.
function Book({ title, color, x }) {
  const ref = useRef();
  const [hovered, setHovered] = useState(false);
  useFrame((state, delta) => {                         // runs every frame inside the render loop
    ref.current.position.y += ((hovered ? 0.6 : 0.45) - ref.current.position.y) * Math.min(1, delta * 10);
  });
  return h('mesh', {
    ref, position: [x, 0.45, 0],
    onPointerOver: (e) => { e.stopPropagation(); setHovered(true); },
    onPointerOut: () => setHovered(false),
    onClick: (e) => { e.stopPropagation(); document.getElementById('out').textContent = `clicked ${title}`; },
  },
    h('boxGeometry', { args: [0.14, 0.9, 0.62] }),
    h('meshStandardMaterial', { color, emissive: hovered ? '#333333' : '#000000' }));
}

function App() {
  return h(Canvas, {
    camera: { position: [1, 1.1, 3], fov: 35 }, flat: true,       // flat: no ACES tone mapping
    onCreated: ({ camera }) => camera.lookAt(0, 0.35, 0) },
    h('color', { attach: 'background', args: ['#fbf8f2'] }),
    h('hemisphereLight', { args: ['#ffffff', '#8d6e63', 1.2] }),
    h('directionalLight', { position: [2, 3, 4], intensity: 2.5 }),
    h('mesh', { position: [0, -0.02, 0] }, h('boxGeometry', { args: [1.4, 0.04, 0.8] }), h('meshStandardMaterial', { color: '#8d6e63' })),
    BOOKS.map(([title, color], i) => h(Book, { key: title, title, color, x: (i - 2.5) * 0.18 })));
}
createRoot(document.getElementById('root')).render(h(App));
</script>