Skip to content

feat: add TubesBackground component and types#927

Open
AshishDev-16 wants to merge 7 commits intomagicuidesign:mainfrom
AshishDev-16:CursorFlux
Open

feat: add TubesBackground component and types#927
AshishDev-16 wants to merge 7 commits intomagicuidesign:mainfrom
AshishDev-16:CursorFlux

Conversation

@AshishDev-16
Copy link
Copy Markdown

This PR introduces a brand new, highly interactive 3D animated TubesBackground component to the MagicUI library using threejs-components.

Changes

  • New Component: Added TubesBackground for gorgeous, customizable, animated 3D background effects.
  • Interactivity: Built-in click interactions to randomize the abstract tube lighting and color gradients dynamically on the canvas.
  • Demo & Registry: Created a fully featured demo (tubes-background-demo.tsx) and successfully registered the component for the CLI.
  • Documentation: Wrote comprehensive MDX documentation (tubes-background.mdx) with clear usage instructions and configured it in the docs sidebar.
  • Types: Ensured strict TypeScript compliance by adding the necessary declaration types (threejs-components.d.ts).

Motivation

MagicUI aims to provide the best modern, dynamic web interface components out there. Adding this abstract geometric 3D background gives users a premium, high-end aesthetic option right out of the box. It feels completely alive and responsive to clicks, giving any landing page or hero section an immediate "wow" factor.

Breaking Changes

None! This is purely a neat, backwards-compatible new component addition.

Screenshots / Videos

Screen.Recording.2026-03-18.141843.mp4

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 18, 2026

@AshishDev-16 is attempting to deploy a commit to the product-studio Team on Vercel.

A member of the Team first needs to authorize it.

@Yeom-JinHo Yeom-JinHo self-requested a review March 18, 2026 09:56
Copy link
Copy Markdown
Member

@Yeom-JinHo Yeom-JinHo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AshishDev-16
Tysm for the contribution — the effect looks really cool!!

I don't think we're in a place to merge this as-is. It still feels too tightly coupled to threejs-components, so right now it reads more like a thin wrapper around that package than a standalone Magic UI component.

Third-party usage isn't the issue by itself, but I'm not yet convinced this adds enough Magic UI-specific value to justify bringing in another dependency. Also, since threejs-components is sponsors-only, it makes long-term maintenance and debugging harder for contributors.

Happy to take another look if you want to explore a more self-contained version. The comments in my review may be a helpful starting point.

@@ -0,0 +1,4 @@
declare module "threejs-components/build/cursors/tubes1.min.js" {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This deep import still breaks strict TS consumers. The local declaration fixes the docs app, but it isn’t shipped, so could we handle it inline?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback! You bring up a great point about the dependency and maintenance issues. I completely agree. I'll rewrite this component using vanilla Three.js directly. This makes it a standalone, 0-dependency (other than three) Magic UI component


try {
const threeModule =
await import("threejs-components/build/cursors/tubes1.min.js")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This deep import feels too brittle for a registry component: it relies on an internal build/... path, and combined with the missing shipped typings it makes the integration fragile for consumers. If threejs-components ever reorganizes or renames files under build/, this would break immediately. Can we avoid depending on this internal file path?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the vanilla Three.js approach, we won't need this deep import at all, solving the brittleness issue.

cleanup = () => {
window.removeEventListener("resize", handleResize)
if (app && app.destroy) {
app.destroy()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The integration is assuming resize() / destroy() on the returned object, but the published Tubes cursor API returns dispose() instead. As written, we never call the real cleanup path on unmount, which can leave the renderer and global listeners alive after the component is removed. Could we switch this to the actual dispose() API?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will ensure the new implementation handles proper Three.js memory management, explicitly disposing of the renderer, scenes, geometries, and materials within the useEffect cleanup return block.

@Yeom-JinHo
Copy link
Copy Markdown
Member

If there's a way to achieve this without relying on threejs-components, I think it'd be a much stronger fit for Magic UI ⭐️

@Yeom-JinHo
Copy link
Copy Markdown
Member

@AshishDev-16

Tysm! This looks much closer now. The shipped registry/docs outputs are still on the old threejs-components version, so the install path is still out of sync with the new source. Could you regenerate/update those too?

@Yeom-JinHo
Copy link
Copy Markdown
Member

@AshishDev-16
Moving this to standalone Three.js is a much better fit for Magic UI.

I think there are still 2 implementation issues to fix before this is ready:

  1. The light color randomization does not currently affect the rendered tubes, because the meshes are using MeshBasicMaterial, which ignores scene lighting. So setLightsColors() looks like a no-op right now.
  2. The content wrapper uses pointer-events-none, which means any interactive children (buttons, links, inputs) become unusable.

Happy to take another look once those are updated.

…ctivity

- Throttle TubeGeometry rebuilds to fix memory leaks and frame drops
- Change material to MeshStandardMaterial to respect point lighting
- Scope pointer and resize observers locally to container logic
- Use constrained HSL values for random colors to fix readability
- Restore pointer-events for nested React children
- Guard empty vector arrays and zero-dimension DOMRect accesses
- Rebuild magicui registry artifacts
@AshishDev-16
Copy link
Copy Markdown
Author

Thanks for the detailed review🙌 I've pushed an update addressing all the remaining issues. Here's what was fixed:

  1. Memory Leak / Performance: Throttled the TubeGeometry rebuilds to every 2nd frame (frameCount % 2 === 0). The vector math (t.vectors.pop() and .unshift()) is now properly gated inside this same condition to prevent desyncing between the calculated arrays and the rendered geometry. This should keep everything buttery smooth on mid-range devices without the GC pressure.

  2. First-Frame Empty Vector Guard: Added if (t.vectors.length === 0) return to gracefully escape the forEach loop upfront just in case the array ever empties unexpectedly, preventing the t.vectors[0] crash.

  3. Invalid DOMRect Guarding: Updated the onPointerMove guard to explicitly check for if (!rect.width | | !rect.height) return instead of relying on a null check for getBoundingClientRect().

  4. Interactive Children: Removed pointer-events-none from the JSX wrapper. Any nested buttons or elements passed as {children} are now fully interactive.

  5. Readable Colors: Changed randomColors() to calculate hsl(h, s, l) strings with bounded lightness (45-70%) and saturation (60-90%). The neon tubes now consistently "pop" correctly against the #030712 background.

  6. enableClickInteraction Styling: The wrapper div now only uses cursor: pointer and binds onClick if enableClickInteraction is explicitly true; otherwise, it defaults out cleanly.

  7. Cleanup Closure / ResizeObserver: Added const container = containerRef.current strictly at the top of the useEffect so the cleanup lifecycle safely unbinds events. For resize handling, I swapped the global window event out for a ResizeObserver observing the container.

  8. Lights/Material Update: Swapped MeshBasicMaterial out for MeshStandardMaterial for the geometry. setLightsColors() works perfectly now as the tubes correctly receive the randomized illumination from the scene point lights.

I've also run pnpm build:registry locally so the registry JSONs and dependencies are fully sync'd back up.
Let me know if this covers everything and if you spot anything else!

@Yeom-JinHo
Copy link
Copy Markdown
Member

@AshishDev-16
Tysm for the contribution. The effect looks great.

Before merge, could we expose tubeCount, colors, speed, and backgroundColor as props, and replace the hardcoded #030712 with backgroundColor for both the base background and the gradient overlay?

The thing I’m most unsure about before moving forward is how we want to handle mobile devices. The effect feels pretty heavy in practice, and with Three.js being a fairly large dependency too, I’m still thinking about whether this is the right fit for the library as-is. If you already have a direction in mind for mobile, performance tradeoffs, or a graceful fallback, I’d be very happy to hear it.

I think those changes would make the component more reusable while also helping us evaluate whether it fits Magic UI well.

@AshishDev-16
Copy link
Copy Markdown
Author

AshishDev-16 commented Mar 30, 2026

@Yeom-JinHo
Thanks so much for the feedback.

Totally agree on the props. I'll get tubeCount, colors, speed, and background Color-code and swap out both the hardcoded #030712 instances with the prop.

On the mobile and bundle concerns, you're right to flag this, and I've been thinking about it too. Here's what I'm planning:

Bundle size: I'll move Three.js to a peer dependency rather than bundling it. Consumers who never use this component pay zero cost, and those who do can lazy-load it however fits their setup. I'll add a clear, friendly error if Three.js isn't installed so it doesn't fail silently.

Universal win first: IntersectionObserver. Regardless of device, I'll pause the requestAnimationFrame loop when the component scrolls out of the viewport. No reason to burn the GPU and battery on a hero section the user isn't even looking at. This one helps everyone.

Mobile scaling: on viewports under 768 px, the component will automatically drop tubeCount to 6, reduce curve segments from 50 to 20, and cap pixelRatio at 1. That's roughly half the geometry operations plus meaningful fill-rate savings on high-DPI screens; the effect still looks good, just lighter.

Graceful fallback: I'll add a disableOnMobile prop that defaults to true. So on mobile, it falls back to a pure CSS gradient using backgroundColor unless the developer explicitly opts in to WebGL. The fallback looks intentional rather than broken since it uses the same color prop.

Does that feel like the right balance? Happy to adjust any of it before I start building.

@Yeom-JinHo
Copy link
Copy Markdown
Member

@AshishDev-16
Tysm for the thoughtful proposal. The disableOnMobile default plus the IntersectionObserver approach makes a lot of sense to me.

To keep this PR scoped, I think we can skip the mobile-optimized WebGL path for now. If disableOnMobile is true by default, the CSS fallback should be enough for the initial version.

That said, I still want a bit more time to think about whether bringing Three.js into Magic UI, even as a peer dependency, is the right long-term fit. Let me get back to you on that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants