component

Sign

In-world text rendered in a world font (the one font-using component); falls back to the default bundled in @runek/core.

⌖ Explore the library in 3D → · View as Markdown

Tune every prop in the workshop →

Add it

npx @runek/cli add sign

Pulls @react-three/drei@^10.7.7, @runek/core@^0.15.0.

Use it

import { Sign } from './runek/Sign'

<Sign position={[0, 0, 0]} />

Or as a node in a world file (worlds as data):

{ "type": "Sign", "props": { "position": [0, 0, 0] } }

Props

Prop Type Default Description
children string required The text to render.
position [x, y, z] [0, 0, 0] Position [x, y, z] in units (1 unit = 1 m, Y-up).
rotation [x, y, z] [0, 0, 0] Euler rotation [x, y, z] in radians.
seed number Accepted for the component contract; this component has no seeded variation yet.
variant "display" | "body" "display" Which world font role to render in (display for titles/signage, body for labels). The world declares the actual face via <World fonts>; an undeclared role falls back to the font bundled in @runek/core. (Named variant, not role, so it doesn’t trip JSX a11y linters.)
font string Explicit font URL, overriding the world role.
size number 0.3 Cap height in units.
color color palette accent Text color. Defaults to the world palette’s accent slot.
maxWidth number Wrap width in units; omit to keep the text on one line.
letterSpacing number 0
anchorX "left" | "center" | "right" "center"
anchorY "top" | "bottom" | "middle" "middle"
glow boolean false Soft colored halo around the glyphs, for a glow without bloom.
outline color A crisp edge in this color, so the text reads against any background. Wins over glow.
onBounds function (code only) Called with the laid-out text’s width and height, in world units, whenever it changes: for fitting a backdrop behind it.

Registry manifest

registry manifest /r/components/sign.json Self-contained JSON: inlined source plus resolved dependencies, exactly what runek add sign fetches.

Browse the whole catalog in the gallery →.

Source (Sign.tsx)
import { Text } from '@react-three/drei'
import { useWorld, type Vec3, type WorldFonts } from '@runek/core'
import type { ReactNode } from 'react'

export interface SignProps {
  /** The text to render. */
  children: ReactNode
  position?: Vec3
  rotation?: Vec3
  /** Accepted for the component contract; this component has no seeded variation yet. */
  seed?: number
  /**
   * Which world font role to render in (`display` for titles/signage, `body`
   * for labels). The world declares the actual face via `<World fonts>`; an
   * undeclared role falls back to the font bundled in `@runek/core`. (Named
   * `variant`, not `role`, so it doesn't trip JSX a11y linters.)
   */
  variant?: keyof WorldFonts
  /** Explicit font URL, overriding the world role. */
  font?: string
  /** Cap height in units. */
  size?: number
  /** Text color. Defaults to the world palette's `accent` slot. */
  color?: string
  /** Wrap width in units; omit to keep the text on one line. */
  maxWidth?: number
  letterSpacing?: number
  anchorX?: 'left' | 'center' | 'right'
  anchorY?: 'top' | 'middle' | 'bottom'
  /** Soft colored halo around the glyphs, for a glow without bloom. */
  glow?: boolean
  /** A crisp edge in this color, so the text reads against any background. Wins over `glow`. */
  outline?: string
  /** Called with the laid-out text's width and height, in world units, whenever it changes:
   *  for fitting a backdrop behind it. */
  onBounds?: (width: number, height: number) => void
}

/**
 * In-world text. The one component that renders glyphs, and so the single
 * carve-out to the no-assets rule (CONTRACT §4): it ships no font of its own,
 * drawing only from the world's declared `fonts` or the default bundled in
 * `@runek/core`. Decorative: it registers no colliders (mount it on a surface
 * that has one, e.g. a Wall).
 */
export function Sign({
  children,
  position = [0, 0, 0],
  rotation = [0, 0, 0],
  variant = 'display',
  font,
  size = 0.3,
  color,
  maxWidth,
  letterSpacing = 0,
  anchorX = 'center',
  anchorY = 'middle',
  glow = false,
  outline,
  onBounds,
}: SignProps) {
  const { unit, fonts, palette } = useWorld()
  const face = font ?? fonts[variant]
  const ink = color ?? palette.accent

  return (
    <Text
      font={face}
      position={position}
      rotation={rotation}
      fontSize={size * unit}
      color={ink}
      maxWidth={maxWidth !== undefined ? maxWidth * unit : undefined}
      letterSpacing={letterSpacing}
      anchorX={anchorX}
      anchorY={anchorY}
      outlineWidth={outline ? '9%' : glow ? 0.02 * unit : 0}
      outlineColor={outline ?? ink}
      outlineBlur={outline || !glow ? 0 : '45%'}
      outlineOpacity={outline || !glow ? 1 : 0.5}
      onSync={
        onBounds
          ? (text: { textRenderInfo?: { blockBounds: number[] } }) => {
              const b = text.textRenderInfo?.blockBounds
              if (b) onBounds(b[2] - b[0], b[3] - b[1])
            }
          : undefined
      }
    >
      {children}
    </Text>
  )
}