Use the /collab command on Discord to gain access to the OneJS private repo. The repo offers early access to the latest features and fixes that may not yet be available on the Asset Store. An early preview of OneJS V2 is available on branch onejs-v2. It brings major performance improvements, zero-allocation interop, and a new esbuild workflow.
VERSION
Doc Menu

OneJS UI Components

Inspired by Headless UI, OneJS offers a set of unstyled UI components. Example components are also included to demonstrate their usage and styling.

Sample code is available at {ProjectDir}/OneJS/Samples/comps-sample.tsx. Beyond just demonstrations, the source code of these example components are handy references. You can use them as they are, or you can copy and adjust them to fit your needs.


<gradientrect class="w-full h-full justify-center items-center" colors={[c("#42c873"), c("#06a0bb")]}>
    <ExampleTabs class="mb-4" />
    <Select class="min-w-[200px] mb-4" items={people} onChange={setSelectedPerson} />
    <Toggle class="mb-4" checked={checked} onChange={setChecked} />
    <DiamondToggle class="mb-4" checked={checked2} onChange={setChecked2} />
    <RadioToggle class="mb-4" items={tiers} onChange={setSelectedTier} />
</gradientrect>

Headless UI Comps

Misc Samples

Toggle

import { Switch, Toggle, DiamondToggle } from "onejs/comps"


Toggle is the simplest comp here. If you are new with all this headless UI stuff and Render Props, you should just start with checking out the source code for Toggle. It's located at ScriptLib/onejs/comps/Toggle.tsx. <Switch /> is the headless component. <Toggle /> and <DiamondToggle /> are example comps using <Switch />.

Sample <Switch /> Usage
const Toggle = ({ class: classProp, children, checked, onChange, style }) => {

    return <Switch class={`w-16 h-8 rounded-[16px] p-[2px] ${checked ? 'bg-[rgba(0_0_0_0.8)]' : 'bg-[rgba(0_0_0_0.5)]'} transition-[background-color] duration-200 ${classProp}`} checked={checked} onChange={onChange} style={style}>
        {({ checked }) => (
            <div class={`w-[28px] h-[28px] bg-white rounded-full ${checked ? 'translate-x-[32px]' : 'translate-x-0'} transition-[translate] duration-200`} onClick={() => onChange && onChange(!checked)} />
        )}
    </Switch>
}

const DiamondToggle = ({ class: classProp, children, checked, onChange, style }) => {

    return <Switch class={`w-8 h-8 p-[6px] rounded-md border-[1px] ${checked ? 'border-[rgba(255_255_255_0.8)] bg-[rgba(0_0_0_0.5)] rotate-[45deg]' : 'border-[rgba(255_255_255_0.5)] bg-[rgba(0_0_0_0.8)] rotate-0'} transition-[background-color,rotate] duration-200 ${classProp}`} checked={checked} onChange={onChange} style={style}>
        {({ checked }) => (
            <div class={`w-[18px] h-[18px] bg-white rounded-sm ${checked ? 'flex' : 'hidden'}`} onClick={() => onChange && onChange(!checked)} />
        )}
    </Switch>
}

Select

import { Listbox, Select } from "onejs/comps"


Select implements a dropdown using the headless Listbox comp. The most interesting thing here is how Listbox works around UI Toolkit's lack of z-index. Check out the source code for Listbox.Options for that.

Sample <Listbox /> Usage
const Select = ({ class: classProp, items, index, onChange, style }) => {
    index = index || 0
    const [selectedItem, setSelectedItem] = useState(items[index])

    useEffect(() => {
        onChange && onChange(selectedItem)
    }, [selectedItem])

    return <Listbox class={`relative ${classProp}`} items={items} index={index} onChange={setSelectedItem}>
        <Listbox.Button class={`default-bg-color active-text-color bold rounded-sm px-[12px] py-[10px] flex-row justify-between`}>
            <div class="">{selectedItem.name}</div>
            <FAIcon name="down-dir" class="active-text-color translate-y-1" />
        </Listbox.Button>
        <Listbox.Options class="absolute default-bg-color default-text-color rounded-sm py-2 mt-2">
            {items.map((item, i) => (
                <Listbox.Option index={i} class={`hover:hover-bg-color hover:active-text-color px-[12px] py-[10px] flex-row justify-between`} item={item}>
                    {({ selected }) => <Fragment>
                        <div class={`bold ${selected ? 'active-text-color' : ''}`}>
                            {item.name}
                        </div>
                        {selected ? <FAIcon name="ok" class="active-text-color translate-y-1" /> : null}
                    </Fragment>}
                </Listbox.Option>
            ))}
        </Listbox.Options>
    </Listbox>
}

Radio Group

import { RadioGroup, RadioToggle } from "onejs/comps"


RadioToggle demonstrates how to use RadioGroup to design your own multi-choice toggle comp.

Sample <RadioGroup /> Usage
const RadioToggle = ({ class: classProp, items, index, onChange, style }: RadioToggleProps) => {
    index = index || 0

    function onChangeIndex(index: number) {
        onChange && onChange(items[index].value)
    }

    return <RadioGroup class={`flex flex-row rounded-sm overflow-hidden default-bg-color active-text-color bold ${classProp}`} index={index} onChange={onChangeIndex}>
        {items.map((item, i) => (
            <RadioGroup.Option class={({ checked }) => `${checked ? "accented-bg-color highlighted-text-color" : "bg-white"} p-3 transition-[background-color] duration-200`} index={i}>
                {({ checked }) => <Fragment>{item.label}</Fragment>}
            </RadioGroup.Option>
        ))}
    </RadioGroup>
}

Tab

import { Tab, ExampleTabs } from "onejs/comps"


Sample <Tab /> Usage

ExampleTabs demonstrates Tab.Group, Tab.List, and Tab.Panels usage.

const ExampleTabs = ({ class: classProp, style }: ExampleTabsProps) => {

    function onChange(index: number) {
        log(`Tabs index changed to ${index}`)
    }

    return <Tab.Group class={`flex-col w-[500px] ${classProp}`} onChange={onChange} style={style}>
        <Tab.List class={`flex-row justify-between rounded-md bg-black/50 p-1 mb-2`}>
            {exampleTabs.map((tab, index) =>
                <Tab name={tab.label} index={index} class={({ selected }) => (classNames(`flex-row rounded-md text-white/80 items-center bold justify-center p-3 transition-[background-color] duration-200`, selected ? `bg-white active-text-color` : `hover:bg-white/10`))} style={{ width: `${98 / exampleTabs.length}%` }}>{tab.label}</Tab>
            )}
        </Tab.List>
        <Tab.Panels>
            {exampleTabs.map((tab, index) =>
                <Tab.Panel class={`bg-white rounded-md p-5`}>{tab.content}</Tab.Panel>
            )}
        </Tab.Panels>
    </Tab.Group>
}

Transition

import { Transition } from "onejs/comps"

Using Transition and Transition.Child together is a great way to animate multiple components at the same time and make them respond to the same state change. Sample usage code is located at Samples/transition-sample.tsx.

Sample <Transition /> Usage
const App = () => {
    const [show1, setShow1] = useState(true)
    const [show2, setShow2] = useState(true)

    return <gradientrect class="w-full h-full flex-row justify-center items-center" colors={[c("#42c873"), c("#06a0bb")]}>
        <div class="w-64 h-64 justify-center items-center">
            <div class="h-32 w-32 mb-8">
                <Transition
                    class="h-full w-full"
                    show={show1}
                    appear={true}
                    enter="transition[opacity,rotate,scale] duration-[400ms]"
                    enterFrom="opacity-0 rotate-[-120deg] scale-50"
                    enterTo="opacity-100 rotate-0 scale-100"
                    leave="transition[opacity,rotate,scale] duration-200 ease-in-out"
                    leaveFrom="opacity-100 rotate-0 scale-100"
                    leaveTo="opacity-0 scale-75"
                >
                    <div class="h-full w-full rounded-md bg-orange-200" />
                </Transition>
            </div>
            <button onClick={() => setShow1((show1) => !show1)} text="Toggle" />
        </div>
        <div class="w-64 h-64 justify-center items-center">
            <div class="h-32 w-32 mb-8">
                <Transition class="h-full w-full flex-row justify-around" show={show2} appear={true}>
                    <div class="h-full w-[48%]">
                        <Transition.Child class="h-full w-full"
                            enter="transition[translate,opacity] duration-[400ms]"
                            enterFrom="opacity-0 translate-y-[20%]"
                            enterTo="opacity-100 translate-y-[0]"
                            leave="transition[translate,opacity] duration-200 ease-in-out"
                            leaveFrom="opacity-100 translate-y-[0]"
                            leaveTo="opacity-0 translate-y-[20%]"
                        >
                            <div class="h-full w-full rounded-md bg-orange-200" />
                        </Transition.Child>
                    </div>
                    <div class="h-full w-[48%]">
                        <Transition.Child class="h-full w-full"
                            enter="transition[translate,opacity] duration-[400ms]"
                            enterFrom="opacity-0 translate-y-[-20%]"
                            enterTo="opacity-100 translate-y-[0]"
                            leave="transition[translate,opacity] duration-200 ease-in-out"
                            leaveFrom="opacity-100 translate-y-[0]"
                            leaveTo="opacity-0 translate-y-[-20%]"
                        >
                            <div class="h-full w-full rounded-md bg-orange-200" />
                        </Transition.Child>
                    </div>
                </Transition>
            </div>
            <button onClick={() => setShow2((show2) => !show2)} text="Toggle" />
        </div>
    </gradientrect>
}

Slider Sample

import { Slider } from "onejs/comps"


This is a pure-JS implementation of a Slider component. Free feel to copy and modify according to your own needs.

Slider implementation
const Slider = ({ class: classProp, style, value, onChange, min: _min, max: _max }: SliderProps) => {
    const ref = useRef<Dom>()
    const progressRef = useRef<Dom>()
    const thumbRef = useRef<Dom>()

    const [mouseDown, setMouseDown] = useState(false)

    const min = _min || 0
    const max = _max || 1

    let currentValue = value === null || typeof value === "undefined" ? min : value
    let currentFraction = (currentValue - min) / (max - min)

    useEffect(() => {
        document.body.addEventListener("MouseMove", handleMouseMove)
        document.body.addEventListener("MouseUp", handleMouseUp)
        return () => {
            document.body.removeEventListener("MouseMove", handleMouseMove)
            document.body.removeEventListener("MouseUp", handleMouseUp)
        }
    }, [mouseDown])

    function calculateFromMouseX(clientX) {
        const rect = ref.current.ve.worldBound
        const fraction = (clientX - rect.left) / rect.width
        const newValue = (min + fraction * (max - min))
        return Math.min(Math.max(newValue, min), max)
    }

    function processValueChange(newValue: number) {
        if (newValue != currentValue) {
            onChange && onChange(newValue)
            currentValue = newValue
            currentFraction = (currentValue - min) / (max - min)
            const rect = ref.current.ve.worldBound
            progressRef.current.style.width = `${currentFraction * 100}%`
            thumbRef.current.style.left = currentFraction * rect.width
        }
    }

    function handleMouseDown(e: MouseDownEvent) {
        setMouseDown(true)
        const newValue = calculateFromMouseX(e.mousePosition.x)
        processValueChange(newValue)
    }

    function handleMouseMove(e: MouseMoveEvent) {
        if (!mouseDown) return
        const newValue = calculateFromMouseX(e.mousePosition.x)
        processValueChange(newValue)
    }

    function handleMouseUp() {
        setMouseDown(false)
    }

    return <div class={`h-[30px] justify-center ${classProp}`} ref={ref} onMouseDown={handleMouseDown} style={style}>
        <div class={`w-full h-[8px] bg-gray-400`} style={{ borderRadius: 4 }}>
            <div ref={progressRef} class={`accented-bg-color h-[8px] justify-center`} style={{ width: `${Math.round(currentFraction * 100)}%`, borderRadius: 4 }}>
            </div>
        </div>
        <div ref={thumbRef} class={`w-[24px] h-[24px] default-bg-color absolute rounded-full translate-x-[-10px]`} />
    </div>
}

Color Picker Texture Fill

Samples/colorpicker-sample.tsx

This one is a simple example of calling custom C# job from JS.

colorpicker-sample.tsx
import { GradientTextureFillJob } from "OneJS/Utils"
import { Color, Texture2D, TextureFormat } from "UnityEngine"
import { Slider } from "onejs/comps"
import { parseColor as c } from "onejs/utils/color-parser"
import { h, render } from "preact"
import { useEffect } from "preact/hooks"

var texture = new Texture2D(200, 200, TextureFormat.RGBA32, false)
var colors = texture.GetRawDataColor32()

const App = () => {

    useEffect(() => {
        onChange(0)
    }, [])

    function onChange(v: number) {
        var hueColor = Color.HSVToRGB(v, 1, 1)
        GradientTextureFillJob.Run(colors, 200, 200, hueColor)
        texture.Apply()
    }

    return <gradientrect class="w-full h-full justify-center items-center" colors={[c("#42c873"), c("#06a0bb")]}>
        <image class="mb-4" image={texture} />
        <Slider class="w-[200px]" onChange={onChange} />
    </gradientrect>
}

render(<App />, document.body)
GradientTextureFillJob.cs
[BurstCompile]
public struct GradientTextureFillJob : IJobParallelFor {
    public NativeArray<Color32> colors;
    public int width;
    public int height;
    public Color32 topRightColor;

    // Convenience static method for easy calling from JS for example
    public static void Run(NativeArray<Color32> colors, int width, int height, Color32 topRightColor) {
        var job = new GradientTextureFillJob {
            colors = colors,
            width = width,
            height = height,
            topRightColor = topRightColor
        };
        job.Schedule(colors.Length, 64).Complete();
    }

    public void Execute(int index) {
        int x = index % width;
        int y = index / height;
        float fx = (float)x / (float)width;
        float fy = (float)y / (float)height;

        Color32 leftColor = Color32.Lerp(Color.black, Color.white, fy);
        Color32 rightColor = Color32.Lerp(Color.black, topRightColor, fy);
        Color32 pixelColor = Color32.Lerp(leftColor, rightColor, fx);

        colors[index] = pixelColor;
    }
}