# Elements and Styling

A cart's screen is React, but the things it renders are not HTML elements and the styles are not CSS. They are close enough to feel familiar and different enough to catch you out, so this page is the short version of both.

```tsx
import { View, Text, Button } from "oj"

<View style={{ flexDirection: "row", padding: 12, backgroundColor: "#1b2130" }}>
    <Text style={{ color: "white", fontSize: 18 }}>Score: 12</Text>
    <Button text="Again" onClick={restart} />
</View>
```

## The elements

| | |
|---|---|
| `View` | The box everything else is built from. There is no `div` |
| `Text` | A run of text. What you want most of the time |
| `Label` | A form label. `Text` for display, `Label` next to a control |
| `Button` | Takes `onClick`, and its label as `text` or as children |
| `TextField` | Single or multi-line text entry |
| `Toggle` | A checkbox |
| `Slider` | A numeric range |
| `ScrollView` | A box whose content can be longer than it is |
| `Image` | A picture. See [Shipping Files](/docs/files) |
| `ListView`, `TreeView` | Virtualised lists, for thousands of rows |
| `FrostedGlass` | A blurred backdrop |

That is the whole set. There is no `span`, no `p`, no `ul`: a `View` with the right styles is all of those.

Raw text inside an element works, so `<View>hello</View>` renders, but reach for `Text` when you want to style it.

## Styling

Styles are objects, applied inline, and every element takes a `style` prop.

```tsx
<View style={{ width: 200, height: 120, borderRadius: 8, backgroundColor: "#2a3240" }} />
```

**Numbers are pixels.** Strings carry their own unit, so `"50%"` is a percentage. Colours are strings: `"#ff5500"`, `"rgb(255, 85, 0)"`, `"rgba(255, 85, 0, 0.5)"`.

**Layout is flexbox and only flexbox.** There is no `display: block`, no floats, no grid. Every element is a flex container, and a `View` with no other styling stacks its children vertically.

```tsx
style={{
    flexDirection: "row",       // or "column", the default
    justifyContent: "center",   // along the direction
    alignItems: "center",       // across it
    flexGrow: 1,                // take the spare room
    flexShrink: 0,              // and do not give up your own
    position: "absolute",       // with left / top / right / bottom
}}
```

**Five shorthands work**: `padding`, `margin`, `borderWidth`, `borderColor` and `borderRadius`. They expand to their four sides for you. Anything else has to be written out per property, so there is no `background` or `font` shorthand.

For anything beyond a handful of styles, [CSS Modules and Tailwind](https://onejs.com/docs/guides/css-modules) both work and are worth the setup on a cart with more than one screen.

One Tailwind trap worth knowing: the width and height utilities come from the spacing and percentage scales (`w-96`, `max-w-1/2`) and not the named size scale, so `max-w-md` and friends compile to nothing at all and your element keeps its full width. Write the number, or set `maxWidth` as a style.

## Events

```tsx
<Button text="Jump" onClick={() => jump()} />
<Toggle value={muted} onChange={(e) => setMuted(e.value)} />
<Slider value={vol} min={0} max={100} onChange={(e) => setVol(e.value)} />
<View onPointerDown={(e) => console.log(e.x, e.y)} />
```

Events bubble. `e.stopPropagation()` stops them; `e.preventDefault()` stops the native control underneath (a `ScrollView` scrolling, for instance) from acting.

For anything a cart reads every frame, use [`input`](https://onejs.com/docs/guides/input) inside `useFrame` rather than an event handler. Events are for the UI around the cart: buttons, menus, sliders.

## Three traps

Each of these has cost a shipped cart a day. None of them produces an error, which is why they are worth reading before you meet them.

### A change event carries `value`, not `newValue`

```tsx
onChange={(e) => setVolume(e.value)}      // right
onChange={(e) => setVolume(e.newValue)}   // undefined, silently
```

A handler typed loosely accepts the wrong name and hands you `undefined`, so the symptom is a control that does nothing. Every slider in one published cart was dead because of this.

There is no `e.target.value` either. `e.target` exists but it is an integer handle, not an element.

### Pointer `x` and `y` are window pixels; use `localX` and `localY` for your own box

`e.x` and `e.y` are measured from the top left of the **window**, the same numbers as `input.mouse.position` and [`useStage()`](/docs/stage), not from the element your handler is on. For a position inside your own element, read `e.localX` and `e.localY`, which are relative to the element the handler sits on:

```tsx
<View style={{ width: 200, height: 200 }}
    onPointerDown={(e) => {
        const nx = e.localX / 200   // 0 at the left edge, 1 at the right
    }} />
```

They are computed from the element's bounds the first time a handler reads them, so a handler that never asks pays nothing. On a bubbling event they follow `currentTarget`: a parent's handler sees coordinates relative to the parent.

### `flexShrink` defaults to 1

Children of a fixed-height box give up their own height to fit rather than overflowing it. Add rows to a box with a set `height` and every row gets shorter; add enough and one row's text is drawn across the row above.

It reads as a rendering bug rather than a layout one, which is what makes it expensive: the data is right and only a screenshot shows it.

```tsx
<View style={{ height: 124 }}>
    {rows.slice(0, 4).map((row) => (
        <View key={row.id} style={{ flexShrink: 0, flexDirection: "row" }}>...</View>
    ))}
</View>
```

Give rows `flexShrink: 0` and cap how many you show. A panel that lists four names and says "and 6 more" is honest; one that draws ten on top of each other is not.

## Refs, and keeping React out of the frame loop

React is for the parts of the screen that change a few times a second. Anything moving every frame should be written straight to the element, through a ref:

```tsx
const ref = useRef<VisualElement>(null)

useFrame((dt) => {
    x.current += 200 * dt
    if (ref.current) ref.current.style.left = x.current
})

return <View ref={ref} style={{ position: "absolute", width: 32, height: 32 }} />
```

`ref.current` is the underlying element, and `null` until the first render has made it, which is what the `if` is for. `VisualElement` comes from `"oj"`. Assigning to `ref.current.style.left` moves it without a render, where `setState` would rebuild the tree sixty times a second.

Refs are also how you read an element's measured size and position, through `worldBound` (in window pixels) and `layout` (relative to its parent). Both are only meaningful after the first layout pass, so read them in an effect or a frame callback, never during render.

## Going deeper

These pages describe the same elements and styles from the Unity side, in full:

| | |
|---|---|
| [Components](https://onejs.com/docs/components/view) | Every element, prop by prop |
| [Styling](https://onejs.com/docs/core-concepts/styling) | The complete style property list, USS, transitions and transforms |
| [Events](https://onejs.com/docs/core-concepts/events) | Every event type and what it carries |
| [Refs](https://onejs.com/docs/core-concepts/refs) | Reaching an element directly |

They assume a Unity project around them, so skip the setup and take the reference.
