React hooks for controlling Spline 3D scenes. Abstractions over @splinetool/react-spline for common patterns.
bun install
bun devImport from @/actions:
import {
useObjectMover,
useAnimation,
useVisibility,
combineLoaders
} from "@/actions";String shorthand: All hooks accept either an object name string or full options:
// String shorthand
const mover = useObjectMover("Cube");
// Full options
const mover = useObjectMover({
objectName: "Cube",
initialOffset: { z: -100 },
onError: (err) => console.error(err)
});Combine multiple hooks: Use combineLoaders when using multiple hooks on the same Spline:
const mover = useObjectMover("Cube");
const rotator = useObjectRotator("Sphere");
const camera = useCameraMover();
<Spline
scene={url}
onLoad={combineLoaders(mover.onLoad, rotator.onLoad, camera.onLoad)}
/>Standalone functions: For one-off operations when you have the spline instance:
import { moveObject, rotateObject, scaleObject, setVisibility } from "@/actions";
// Inside onLoad or with existing spline ref
moveObject(spline, { objectName: "Cube", position: { x: 100 } });
rotateObject(spline, { objectName: "Globe", rotation: { y: Math.PI } });
scaleObject(spline, { objectName: "Button", scale: { x: 2, y: 2, z: 2 } });
setVisibility(spline, "Tooltip", false);Base hook for spline instance access.
const { onLoad, findObject, setVariable, emitEvent } = useSpline();Use cases:
- Direct access to the Spline application instance
- Finding objects by name or ID for custom manipulations
- Setting variables that aren't covered by other hooks
- Emitting custom events for complex interactions
Why isolate: Foundation for all other hooks. Provides escape hatch when you need raw Spline API access without reimplementing boilerplate.
Track when Spline finishes loading.
const { isReady, onLoad } = useSplineReady();
return (
<>
{!isReady && <LoadingSpinner />}
<Spline scene={url} onLoad={onLoad} />
</>
);Use cases:
- Show loading states while Spline initializes
- Trigger effects after scene is ready
- Conditional rendering based on load state
Why isolate: Loading state is a common need. Avoids manual useState/useRef setup.
Control all transforms (position, rotation, scale) of an object.
const { onLoad, setPosition, setRotation, setScale, getTransform } = useObjectTransform("Cube");Use cases:
- Complex animations requiring multiple transform types
- Full object manipulation in one hook
- Getting current transform state
Why isolate: Combines mover, rotator, and scaler when you need all three. Avoids using combineLoaders for simple cases.
Move objects by name.
// String shorthand
const mover = useObjectMover("Cube");
// Full options
const { onLoad, move, setPosition } = useObjectMover({
objectName: "Cube",
initialOffset: { z: -100 }
});Use cases:
- Slide-in animations triggered by user actions
- Repositioning UI elements based on state
- Drag-and-drop style interactions
- Parallax effects on scroll
Why isolate: Position changes are the most common 3D manipulation. Eliminates repetitive ref setup and null checks across components.
Rotate objects by name.
// String shorthand
const rotator = useObjectRotator("Globe");
// Full options
const { onLoad, rotate, setRotation } = useObjectRotator({
objectName: "Globe",
initialRotation: { y: Math.PI / 4 }
});Use cases:
- Spinning product showcases
- Loading spinners in 3D
- Interactive globes or dials
- Rotate-on-hover effects for cards
Why isolate: Rotation logic is separate from position logic. Keeps components focused on one transform type, easier to combine or swap.
Scale objects by name.
// String shorthand
const scaler = useObjectScaler("Button");
// Full options
const { onLoad, scale, setScale, scaleUniform } = useObjectScaler({
objectName: "Button",
initialScale: { x: 1.5, y: 1.5, z: 1.5 }
});Use cases:
- Hover states that grow/shrink elements
- Pulse animations for attention
- Data visualization (scale bars by value)
- Zoom effects on selection
Why isolate: Scale is often animated independently. scaleUniform is a common need that would otherwise require setting x/y/z separately.
Control camera position.
const { onLoad, pan, setPosition } = useCameraMover({
cameraName: "Camera",
initialOffset: { y: 500 }
});Use cases:
- Cinematic intro sequences
- Focus on different scene areas based on navigation
- Zoom in/out on user interaction
- Guided tours through 3D spaces
Why isolate: Camera is a special object that controls the entire viewport. Dedicated hook prevents accidentally moving regular objects when you meant to move the camera.
Change text via Spline variables.
const { onLoad, setText } = useTextChanger({
variableName: "HeaderText",
initialText: "Welcome"
});Use cases:
- Dynamic labels that update from API data
- User-customized text in 3D scenes
- Real-time counters or scores
- Localization/i18n in 3D
Why isolate: Text in Spline uses variables, not direct object manipulation. This abstraction hides that implementation detail.
Manage multiple variables.
const { onLoad, set, setMany } = useVariables({
initialValues: { Title: "Hello", Count: 0 }
});Use cases:
- Dashboard scenes with multiple data points
- Forms reflected in 3D
- Game state (score, health, level)
- Theming (swap multiple colors at once)
Why isolate: When you have many variables, setting them one-by-one is tedious. setMany batches updates cleanly.
Trigger Spline animations via emitEvent.
// String shorthand
const anim = useAnimation("Door");
// Full options
const { onLoad, play, reverse, toggle, isPlaying } = useAnimation({
objectName: "Door",
eventType: "mouseDown"
});Use cases:
- Open/close doors, drawers, menus
- Play intro animations on page load
- Trigger celebrations on success
- Toggle between two animation states
Why isolate: Spline's emitEvent API is not intuitive. This provides semantic play/reverse/toggle that match mental models.
Play a sequence of animations with delays.
const { onLoad, playSequence, stop } = useAnimationSequence({
steps: [
{ objectName: "Door", delay: 0 },
{ objectName: "Light", delay: 500 },
{ objectName: "Sound", delay: 1000 }
],
loop: false
});Use cases:
- Orchestrated intro sequences
- Step-by-step tutorials
- Domino-effect animations
- Looping ambient animations
Why isolate: Coordinating multiple timed animations requires managing timeouts. This handles cleanup and provides stop/loop controls.
Toggle object visibility.
// String shorthand
const tooltip = useVisibility("Tooltip");
// Full options
const { onLoad, show, hide, toggle, isVisible } = useVisibility({
objectName: "Tooltip",
initialVisible: false
});Use cases:
- Tooltips that appear on hover
- Show/hide UI panels
- Reveal elements on scroll
- Conditional 3D content
Why isolate: Visibility is binary but needs state sync with React. This keeps React state and Spline state in sync automatically.
Control visibility of multiple objects.
const { onLoad, showAll, hideAll, showOnly } = useMultiVisibility({
objectNames: ["Step1", "Step2", "Step3"],
initialVisible: false
});
showOnly("Step2");Use cases:
- Multi-step wizards or onboarding
- Tab-like interfaces in 3D
- Before/after comparisons
- Feature toggles in product demos
Why isolate: showOnly pattern (show one, hide others) is extremely common for stepped UIs. Would require manual loops otherwise.
Track hover state on objects.
const { isHovered, hoveredObject, onSplineMouseHover } = useHoverEffect({
objectNames: ["Button", "Card"],
onEnter: (name) => console.log(`Hovering ${name}`),
onLeave: (name) => console.log(`Left ${name}`)
});Use cases:
- Cursor changes on hoverable objects
- Info panels that follow hover
- Highlight effects managed in React
- Analytics tracking for 3D interactions
Why isolate: Spline's hover event fires continuously. This normalizes it to enter/leave events and tracks current hover state.
Trigger hover animations automatically.
const { spline, onLoad } = useSpline();
const { onSplineMouseHover } = useHoverAnimation({
spline,
objectNames: ["Button", "Card"]
});Use cases:
- Glow effects on hover
- Scale-up animations on buttons
- Color shifts on interactive elements
- Any Spline-defined hover animation
Why isolate: Connects React hover events to Spline's animation system. Handles enter/leave and forward/reverse automatically.
Open modals on object click.
const { isOpen, objectName, onSplineMouseDown, close } = useModalTrigger({
triggerObjects: ["ClickMe"]
});Use cases:
- Product detail modals in 3D catalogs
- Info popups on hotspots
- Confirmation dialogs from 3D buttons
- Lightboxes triggered by 3D thumbnails
Why isolate: Modal state management with Spline clicks is a common pattern. Provides isOpen, objectName, and close handler in one hook.
Generic click handler.
const { onSplineMouseDown } = useObjectClick({
objectNames: ["Button"],
onClick: (name) => console.log(name)
});Use cases:
- Navigation triggered by 3D objects
- Selection mechanics in 3D interfaces
- Custom click handlers per object
- Routing based on clicked object
Why isolate: When you don't need modal state, just a clean click callback. Filters to specific objects without switch statements.
Combine multiple onLoad callbacks when using multiple hooks:
import { combineLoaders } from "@/actions";
const mover = useObjectMover("Cube");
const rotator = useObjectRotator("Sphere");
<Spline
scene={url}
onLoad={combineLoaders(mover.onLoad, rotator.onLoad)}
/>All hooks support an onError callback for custom error handling:
const mover = useObjectMover({
objectName: "MissingObject",
onError: (error) => {
// error.code: "OBJECT_NOT_FOUND" | "SPLINE_NOT_LOADED" | "INVALID_OPERATION"
// error.message: Human readable message
// error.objectName: The object that caused the error
console.error(`${error.code}: ${error.message}`);
}
});/onclick-move-object- Move object on button click/onload-move-camera- Move camera on load/onload-move-game-object- Move grouped objects/onload-text-change- Change text on load/onmousedown-open-modal- Open modal on click