threlte logo
@threlte/flex

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 { 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'

  export let windowWidth: number
  export let windowHeight: number
  export let rows = 5
  export let columns = 5
  export let size: any

  let page = 1
  $: offset = (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"
            let:width
            let:height
          >
            <Matcap
              {width}
              {height}
              matcapIndex={offset + index}
              gridIndex={index}
              format={size}
            />
          </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}
        on:click={() => {
          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}
        on:click={() => {
          page = Math.min(10, page + 1)
        }}
      />
    </Box>
  </Box>
</Window>
MatCap textures from https://github.com/emmelleppi/matcaps

Installation

npm install @threlte/flex

Usage

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

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}
    let:width
    let:height
  >
    <Plane
      {width}
      {height}
    />
  </Box>

  <Box
    width="auto"
    height="auto"
    flex={1}
    let:width
    let:height
  >
    <Plane
      {width}
      {height}
    />
  </Box>
</Flex>

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}
    let:width
    let:height
  >
    <Plane
      color="orange"
      {width}
      {height}
      depth={1}
    />
    <Box
      height="auto"
      flex={1}
      let:width
      let:height
    >
      <Plane
        color="blue"
        {width}
        {height}
        depth={2}
      />
    </Box>

    <Box
      height="auto"
      flex={1}
      let:width
      let:height
    >
      <Plane
        color="red"
        {width}
        {height}
        depth={2}
      />
    </Box>
  </Box>

  <Box
    height="auto"
    width="auto"
    flex={1}
    let:width
    let:height
  >
    <Plane
      depth={1}
      {width}
      {height}
    />
  </Box>
</Flex>

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}
  let:align
>
  <Flex
    width={100}
    height={100}
    on:reflow={align}
  >
    <Box>
      <Plane
        width={20}
        height={20}
      />
    </Box>

    <Box>
      <Plane
        width={20}
        height={20}
      />
    </Box>
  </Flex>
</Align>

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.