Atelier UI®

Read the docsGithub
Docs 1.0.0

Getting started

  • Browse Catalog
  • Installation
  • How to contribute
  • Code of conduct
  • Fluid Scene

Components (34)

  • Clip Reveal
  • Stripe Wipe
  • Sweep Exit
  • Dither Flow
    pro
  • Glowing Fog
    pro
  • Halftone Glow
    pro
  • Orbit Gallery
  • Sphere Gallery
  • Edge Bounce
  • Fluid Distortion
  • Image Trail
  • Lens Media
  • Liquid Media
  • Magnetic Dot Grid
  • Pixel Media
  • Pixel Trail
  • Pixelated Text
  • Text Bounce
  • Text Fluid
  • Text Roll
  • Text Scramble
  • Curve Media
  • Elastic Stick
    pro
  • Infinite Gallery
  • Infinite Parallax
  • Infinite Zoom
  • Pixel Scroll
  • Scattered Scroll
  • Text Split
  • WebGL Image
  • WebGL Provider
  • WebGL Scene
  • WebGL Text
  • WebGL Video
Atelier UI 1.0.0 ©2026
Star on githubBuy me a coffeellms.txt
  1. Docs
  2. /
  3. Components
  4. /
  5. Webgl Provider

WebGL Provider

The root that runs one shared WebGL canvas for the whole page.

Foundation Block
React Three Fiber

WebGL Provider mounts a single <Canvas> for the whole page and renders your application inside it. Every other WebGL component draws onto this canvas, so the application holds one WebGL context instead of one per effect.

Add the provider once, at your application root. For installation steps, see the installation guide.


Install

npx atelier-ui add webgl-provider
npm install three @react-three/fiber @react-three/postprocessing postprocessing
webgl-provider.tsx
"use client"

import { Canvas, type CanvasProps, useThree } from "@react-three/fiber"
import { EffectComposer } from "@react-three/postprocessing"
import { type ComponentRef, type ReactNode, useEffect, useRef, useState } from "react"
import type { Camera, Scene } from "three"
import { effectTeleport, WebglPortal } from "../webgl-portal/webgl-portal"

type WebglProviderProps = Omit<CanvasProps, "children" | "eventSource"> & {
    children: ReactNode
    className?: string
    contained?: boolean
}

type WebglReadyOptions = {
    scene?: Scene
    camera?: Camera
    enabled?: boolean
    onReady?: () => void
}

export function useWebglReady({ scene, camera, enabled = true, onReady }: WebglReadyOptions = {}) {
    const [ready, setReady] = useState(false)
    const gl = useThree((state) => state.gl)
    const defaultScene = useThree((state) => state.scene)
    const defaultCamera = useThree((state) => state.camera)
    const onReadyRef = useRef(onReady)
    onReadyRef.current = onReady

    const targetScene = scene ?? defaultScene
    const targetCamera = camera ?? defaultCamera

    useEffect(() => {
        if (!enabled) return
        let active = true

        gl.compileAsync(targetScene, targetCamera).then(() => {
            if (!active) return
            requestAnimationFrame(() => {
                if (!active) return
                setReady(true)
                onReadyRef.current?.()
            })
        })

        return () => {
            active = false
        }
    }, [gl, targetScene, targetCamera, enabled])

    return ready
}

function Effects() {
    const effects = effectTeleport.useItems()
    if (effects.length === 0) return null

    return (
        <EffectComposer key={effects.length}>
            <effectTeleport.Out />
        </EffectComposer>
    )
}

export function WebglProvider({
    children,
    className,
    style,
    contained = false,
    ...canvasProps
}: WebglProviderProps) {
    const [eventSource, setEventSource] = useState<ComponentRef<"div"> | null>(null)

    return (
        <div
            ref={setEventSource}
            className={className}
            style={contained ? { position: "relative" } : { display: "contents" }}
        >
            <Canvas
                eventPrefix="client"
                dpr={[1, 1.5]}
                {...canvasProps}
                eventSource={eventSource ?? undefined}
                style={{
                    position: contained ? "absolute" : "fixed",
                    inset: 0,
                    pointerEvents: "none",
                    ...style,
                }}
            >
                <WebglPortal />
                <Effects />
            </Canvas>

            {children}
        </div>
    )
}
webgl-portal.tsx
import {
    type ReactNode,
    Suspense,
    useEffect,
    useId,
    useLayoutEffect,
    useSyncExternalStore,
} from "react"

const useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect

// Minimal teleport: <In> registers children in an external store,
// <Out> renders them — bridges across the Canvas React root the same
function WebglTeleport() {
    const items = new Map<string, ReactNode>()
    const listeners = new Set<() => void>()
    let snapshot: [string, ReactNode][] = []

    const emit = () => {
        snapshot = Array.from(items.entries())
        for (const listener of listeners) {
            listener()
        }
    }

    const subscribe = (listener: () => void) => {
        listeners.add(listener)
        return () => {
            listeners.delete(listener)
        }
    }
    const getSnapshot = () => snapshot

    function useItems() {
        return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
    }

    return {
        In({ children }: { children: ReactNode }) {
            const id = useId()

            useIsoLayoutEffect(() => {
                items.set(id, children)
                emit()
                return () => {
                    items.delete(id)
                    emit()
                }
            }, [id, children])
            return null
        },
        useItems,
        Out() {
            const list = useItems()
            return (
                <>
                    {list.map(([id, node]) => (
                        <Suspense key={id} fallback={null}>
                            {node}
                        </Suspense>
                    ))}
                </>
            )
        },
    }
}

const webglTeleport = WebglTeleport()
const effectTeleport = WebglTeleport()

export function WebglPortal() {
    return <webglTeleport.Out />
}

export { effectTeleport, webglTeleport }

Usage

Wrap your root layout with the provider. The canvas renders fixed and full screen behind the page, with your content on top:

Root layout
import { WebglProvider } from "@/components/webgl-provider"

export default function RootLayout({ children }) {
    return <WebglProvider>{children}</WebglProvider>
}

Pass Canvas options

Any React Three Fiber Canvas prop passes through the provider, so you can set props such as gl, camera, and dpr. The provider defaults to eventPrefix="client" and dpr={[1, 1.5]}, which your props override:

Custom Canvas options
<WebglProvider dpr={[1, 2]} gl={{ antialias: false }}>
    {children}
</WebglProvider>

Scope the canvas to its container

By default, the canvas covers the viewport. Set contained to keep it inside the provider's own element, which must have its own size. The component previews use this to render an effect inside a card rather than across the screen:

Contained canvas
<WebglProvider contained className="relative h-96 w-full">
    <Demo />
</WebglProvider>

Track scene readiness

useWebglReady returns false until the scene's shaders have compiled. This can help prevent visible shader-compilation stutter when an effect first appears.

import { useWebglReady } from "@/components/webgl-provider"

function Effect() {
    const ready = useWebglReady()

    return <mesh visible={ready}>{/* ... */}</mesh>
}

API

NameTypeDefaultDescription
childrenReactNode—Your app or page content. Rendered as normal DOM next to the canvas. Required.
containedbooleanfalseScope the canvas to the provider's element instead of the viewport.
classNamestring—Class on the wrapper element.
styleCSSProperties—Merged into the canvas style.

All other React Three Fiber Canvas props are forwarded, except children and eventSource.

useWebglReady

NameTypeDefaultDescription
sceneScenecurrent sceneScene to compile.
cameraCameracurrent cameraCamera to compile against.
enabledbooleantrueRun the compile step. Set false to skip it.
onReady() => void—Called once the scene is compiled and one frame has passed.

Returns a boolean that is false until the scene is ready, then true.

  • Install
  • Usage
  • Pass Canvas options
  • Scope the canvas to its container
  • Track scene readiness
  • API
  • useWebglReady
Star on githubBuy me a coffeellms.txt