Getting Started
Placing content and making layouts in 3D is hard. The flexbox engine
Yoga is a cross-platform layout engine which
implements the flexbox spec. The package @threlte/flex provides components to
easily use Yoga in Threlte.
<script lang="ts"> import { Canvas, T } from '@threlte/core' import Scene from './Scene.svelte' import { NoToneMapping } from 'three' import { Grid, OrbitControls } from '@threlte/extras' import { Pane, Slider, List } from 'svelte-tweakpane-ui'
let innerWidth = 0 let width = 800 let height = 800 let rows = 5 let columns = 5 let size = 128 let sizeOptions = { '64px': 64, '128px': 128, '256px': 256, '512px': 512, '1024px': 1024 }</script>
<Pane title="Flex" position="fixed"> <Slider bind:value={width} label="Window Width" min={450} max={800} /> <Slider bind:value={height} label="Window Height" min={450} max={800} /> <Slider bind:value={rows} label="Rows" step={1} min={3} max={8} /> <Slider bind:value={columns} label="Columns" step={1} min={3} max={8} /> <List bind:value={size} label="MatCap Size" options={sizeOptions} /></Pane>
<svelte:window bind:innerWidth />
<div> <Canvas toneMapping={NoToneMapping}> <Grid position.z={-10.1} plane="xy" gridSize={800} cellColor="#0A0F19" sectionColor="#481D1A" sectionSize={100} cellSize={10} fadeStrength={0} />
<T.OrthographicCamera makeDefault position.z={1000} position.x={500} position.y={500} zoom={innerWidth / 1200} > <OrbitControls /> </T.OrthographicCamera>
<Scene windowWidth={width} windowHeight={height} {rows} {columns} {size} /> </Canvas></div>
<style> div { height: 100%; }</style><script lang="ts"> import { T } from '@threlte/core' import { RoundedBoxGeometry, useCursor } from '@threlte/extras' import { Box } from '@threlte/flex' import Label from './Label.svelte'
let _class: string export { _class as class } export let z = 0 export let text = '' export let order: number | undefined = undefined export let onClick: () => void
const { hovering, onPointerEnter, onPointerLeave } = useCursor()</script>
<Box class={_class} {order}> {#snippet children({ width, height })} <T.Mesh position.z={z} onclick={(event) => { event.stopPropagation() onClick() }} onpointerenter={onPointerEnter} onpointerleave={onPointerLeave} > <RoundedBoxGeometry args={[width, height, 10]} radius={5} /> <T.MeshBasicMaterial color={$hovering ? '#9D9FA3' : '#404550'} />
<Label z={5.1} fontSize="xl" {text} /> </T.Mesh> {/snippet}</Box><script lang="ts"> import { T } from '@threlte/core'
export let color: string = 'white' export let radius = 5 export let z = 0</script>
<T.Mesh position.z={z}> <T.CircleGeometry args={[radius]} /> <T.MeshBasicMaterial {color} /></T.Mesh><script lang="ts"> import { Text } from '@threlte/extras' import type { ColorRepresentation } from 'three' import { useReflow } from '@threlte/flex'
export let text: string export let color: ColorRepresentation = 'white' export let z = 0 export let fontStyle: | 'black' | 'bold' | 'extra-bold' | 'extra-light' | 'light' | 'medium' | 'regular' | 'semi-bold' | 'thin' = 'regular' export let anchorX = '50%' export let anchorY = '50%' export let fontSize: 'xs' | 's' | 'm' | 'l' | 'xl' = 'm'
const fontSizes: Record<typeof fontSize, number> = { xs: 4, s: 6, m: 8, l: 10, xl: 12 }
$: fontUrl = `/fonts/inter/inter-${fontStyle}.ttf`
const reflow = useReflow()</script>
<Text font={fontUrl} position.z={z} {text} {anchorX} {anchorY} fontSize={fontSizes[fontSize]} {color} onsync={reflow}/><script lang="ts"> import { asyncWritable, isInstanceOf, T, useCache } from '@threlte/core' import { createTransition, global, RoundedBoxGeometry, useCursor, useTexture } from '@threlte/extras' import { cubicIn, cubicOut } from 'svelte/easing' import { Spring } from 'svelte/motion'
const cache = useCache()
const matcapsList = asyncWritable( cache.remember(async () => { const matcapListResponse = await fetch( 'https://cdn.jsdelivr.net/gh/pmndrs/drei-assets@master/matcaps.json' ) return (await matcapListResponse.json()) as Record<string, string> }, ['matcaps']) )
interface Props { gridIndex: number matcapIndex: number format?: 64 | 128 | 256 | 512 | 1024 width?: number height?: number }
let { gridIndex, matcapIndex, format = 256, width = 5, height = 5 }: Props = $props()
const { onPointerEnter, onPointerLeave, hovering } = useCursor() const scale = new Spring(0.9)
$effect(() => { scale.set($hovering ? 1 : 0.9) })
const matcapRoot = 'https://rawcdn.githack.com/emmelleppi/matcaps/9b36ccaaf0a24881a39062d05566c9e92be4aa0d'
function getFormatString(fmt: typeof format) { switch (fmt) { case 64: return '-64px' case 128: return '-128px' case 256: return '-256px' case 512: return '-512px' default: return '' } }
const animDelay = gridIndex * 10 const scaleTransition = (useDelay: boolean) => { return createTransition((ref, { direction }) => { if (!isInstanceOf(ref, 'Object3D')) return return { tick(t) { ref.scale.setScalar(t) }, delay: useDelay ? animDelay + (direction === 'in' ? 200 : 0) : 0, duration: 200, easing: direction === 'in' ? cubicOut : cubicIn } }) }</script>
{#if $matcapsList} {@const fileName = `${$matcapsList[String(matcapIndex)]}${getFormatString(format)}.png`} {@const url = `${matcapRoot}/${format}/${fileName}`}
{#key url} {#await useTexture(url) then matcap} <T.Group in={global(scaleTransition(true))} out={global(scaleTransition(true))} > <T.Mesh scale.x={(width / 100) * scale.current} scale.y={(height / 100) * scale.current} scale.z={scale.current} position.z={20} onpointerenter={onPointerEnter} onpointerleave={onPointerLeave} > <RoundedBoxGeometry args={[100, 100, 20]} radius={2} /> <T.MeshMatcapMaterial {matcap} /> </T.Mesh> </T.Group> {/await} {/key}{/if}<script lang="ts"> import type { Snippet } from 'svelte' import { T } from '@threlte/core'
interface Props { color?: string height?: number width?: number depth?: number children?: Snippet }
let { color = 'white', height = 1, width = 1, depth = 0, children }: Props = $props()</script>
<T.Mesh position.z={depth * 20} renderOrder={depth}> <T.PlaneGeometry args={[width, height]} />
{#if children} {@render children?.()} {:else} <T.MeshBasicMaterial {color} transparent opacity={0.5} /> {/if}</T.Mesh><script lang="ts"> import { T } from '@threlte/core' import { Shape, ShapeGeometry } from 'three'
interface Props { color?: string height?: number width?: number radius?: number depth?: number }
let { color = 'white', height = 1, width = 1, radius = 5, depth = 0 }: Props = $props()
let x = 1 let y = 1
const createGeometry = (width: number, height: number, radius: number): ShapeGeometry => { let shape = new Shape() shape.moveTo(x, y + radius) shape.lineTo(x, y + height - radius) shape.quadraticCurveTo(x, y + height, x + radius, y + height) shape.lineTo(x + width - radius, y + height) shape.quadraticCurveTo(x + width, y + height, x + width, y + height - radius) shape.lineTo(x + width, y + radius) shape.quadraticCurveTo(x + width, y, x + width - radius, y) shape.lineTo(x + radius, y) shape.quadraticCurveTo(x, y, x, y + radius)
const geometry = new ShapeGeometry(shape) geometry.center() return geometry }
let geometry = $derived(createGeometry(width, height, radius))</script>
<T.Mesh position.z={depth * 20} renderOrder={depth}> <T is={geometry} /> <T.MeshBasicMaterial {color} /></T.Mesh><script lang="ts"> import { useTask, useThrelte } from '@threlte/core' import { interactivity, transitions } from '@threlte/extras' import { Box } from '@threlte/flex' import { tick } from 'svelte' import Button from './Button.svelte' import Label from './Label.svelte' import Matcap from './Matcap.svelte' import Window from './Window.svelte'
interface Props { windowWidth: number windowHeight: number rows?: number columns?: number size: any }
let { windowWidth, windowHeight, rows = 5, columns = 5, size }: Props = $props()
let page = $state(1) let offset = $derived((page - 1) * rows * columns)
interactivity() transitions()
const { renderStage, autoRender, renderer, scene, camera } = useThrelte()
autoRender.set(false)
useTask( async () => { await tick() renderer.render(scene, camera.current) }, { stage: renderStage, autoInvalidate: false } )</script>
<Window title="Matcaps" width={windowWidth} height={windowHeight}> <Box class="h-full w-full flex-col items-stretch gap-10 p-10"> {#each new Array(rows) as _, rowIndex} <Box class="h-auto w-full flex-1 items-center justify-evenly gap-10"> {#each new Array(columns) as _, columnIndex} {@const index = rowIndex * columns + columnIndex} <Box class="h-full w-full flex-1"> {#snippet children({ width, height })} <Matcap {width} {height} matcapIndex={offset + index} gridIndex={index} format={size} /> {/snippet} </Box> {/each} </Box> {/each}
<Box order={999} class="h-40 w-auto items-center justify-center gap-10" > <Button class="h-full w-auto flex-1" z={15} text="← PREVIOUS PAGE" order={0} onClick={() => { page = Math.max(1, page - 1) }} />
<Box class="h-full w-auto flex-1" order={1} > <Label z={10.1} fontSize="xl" text={`PAGE: ${page}`} /> </Box>
<Button class="h-full w-auto flex-1" z={15} text="NEXT PAGE →" order={2} onClick={() => { page = Math.min(10, page + 1) }} /> </Box> </Box></Window><script lang="ts"> import { T } from '@threlte/core' import { RoundedBoxGeometry } from '@threlte/extras' import { Box, Flex, tailwindParser } from '@threlte/flex' import Circle from './Circle.svelte' import Label from './Label.svelte' import type { Snippet } from 'svelte'
interface Props { title: string width?: number height?: number children?: Snippet<[{ width: number; height: number }]> }
let { title, width = 500, height = 400, children: innerChildren }: Props = $props()</script>
<Flex classParser={tailwindParser} {width} {height} class="flex-col gap-1 p-1"> <T.Mesh> <RoundedBoxGeometry args={[width, height, 20]} radius={6} /> <T.MeshBasicMaterial color="#0A0F19" /> </T.Mesh>
<Box class="h-26 pr-53 w-full items-center justify-start gap-5 pl-8"> {#snippet children({ height, width })} <T.Mesh position.z={20}> <RoundedBoxGeometry args={[width, height, 20]} radius={5} /> <T.MeshBasicMaterial color="#ddd" /> </T.Mesh>
<Box class="h-10 w-10"> <Circle radius={5} color="#FF6057" z={30.01} /> </Box> <Box class="h-10 w-10"> <Circle radius={5} color="#FDBD2E" z={30.01} /> </Box> <Box class="h-10 w-10"> <Circle radius={5} color="#27C840" z={30.01} /> </Box>
<Box class="h-full w-auto flex-1 items-center justify-center"> <Label text={title} z={30.01} fontStyle="semi-bold" fontSize="l" color="#454649" /> </Box> {/snippet} </Box>
<Box class="h-auto w-auto flex-1"> {#snippet children({ width, height })} {@render innerChildren?.({ width, height })} {/snippet} </Box></Flex>Installation
Section titled “Installation”npm install @threlte/flexBasic Example
Section titled “Basic Example”Use the component <Flex> to create a flexbox
container. Since there’s no viewport to fill, you must specify the size of the
container. Add flex items with the component <Box>.
<script lang="ts"> import { Flex } from '@threlte/flex' import Plane from './Plane.svelte'</script>
<Flex width={100} height={100}> <Box> <Plane width={20} height={20} /> </Box>
<Box> <Plane width={20} height={20} /> </Box></Flex>Flex Props
Section titled “Flex Props”The components <Flex> and <Box> accept props to configure the flexbox. If no
width or height is specified on <Box> components, a bounding box is used to
determine the size of the flex item. The computed width or height may be
different from what is specified on the <Box> component, depending on the
flexbox configuration. To make use of the calculated dimensions of a flex item, use the slot props width and height.
<Flex width={100} height={100} flexDirection="Column" justifyContent="SpaceEvenly" alignItems="Stretch"> <Box width="auto" height="auto" flex={1} > {#snippet children({ width, height })} <Plane {width} {height} /> {/snippet} </Box>
<Box width="auto" height="auto" flex={1} > {#snippet children({ width, height })} <Plane {width} {height} /> {/snippet} </Box></Flex>Nested Flex
Section titled “Nested Flex”Every <Box> component is also a flex container. Nesting <Box> components
allows you to create complex layouts.
<Flex width={100} height={100} flexDirection="Column" justifyContent="SpaceEvenly" alignItems="Stretch"> <Box width="auto" height="auto" flex={1} justifyContent="SpaceEvenly" alignItems="Stretch" padding={20} margin={20} gap={20} > {#snippet children({ width, height })} <Plane color="orange" {width} {height} depth={1} /> <Box height="auto" flex={1} > {#snippet children({ width, height })} <Plane color="blue" {width} {height} depth={2} /> {/snippet} </Box>
<Box height="auto" flex={1} > {#snippet children({ width, height })} <Plane color="red" {width} {height} depth={2} /> {/snippet} </Box> {/snippet} </Box>
<Box height="auto" width="auto" flex={1} > {#snippet children({ width, height })} <Plane depth={1} {width} {height} /> {/snippet} </Box></Flex>Align Flex Container
Section titled “Align Flex Container”The component <Align> can be used to align the resulting flex container.
<script lang="ts"> import { Align } from '@threlte/extras' import { Flex } from '@threlte/flex' import Plane from './Plane.svelte'</script>
<Align y={1}> {#snippet children({ align })} <Flex width={100} height={100} onreflow={align} > <Box> <Plane width={20} height={20} /> </Box>
<Box> <Plane width={20} height={20} /> </Box> </Flex> {/snippet}</Align>Using the Prop class
Section titled “Using the Prop class”The prop class can be used on <Box> and <Flex> to easily configure the
flexbox with predefined class names just as you would do in CSS. In order to use
the prop, you need to create a ClassParser using the utility
createClassParser which accepts a
single string and returns NodeProps. Let’s assume, you want to create a parser
that supports the following class names:
.container { display: flex; flex-direction: row; justify-content: center; align-items: stretch; gap: 10px; padding: 10px;}.item { width: auto; height: auto; flex: 1;}You then need to create a ClassParser which returns the corresponding props:
import { createClassParser } from '@threlte/flex'
const classParser = createClassParser((string, props) => { const classNames = string.split(' ') for (const className of classNames) { switch (className) { case 'container': props.flexDirection = 'Row' props.justifyContent = 'Center' props.alignItems = 'Stretch' props.gap = 10 props.padding = 10 break case 'item': props.width = 'auto' props.height = 'auto' props.flex = 1 } } return props})Now you can use the prop class on <Flex> and <Box> to configure the flexbox:
<Flex width={100} height={100} {classParser} class="container"> <Box class="item"> <Plane width={20} height={20} /> </Box>
<Box class="item"> <Plane width={20} height={20} /> </Box></Flex>@threlte/flex ships with a default ClassParser which supports Tailwind-like
class names.