---
title: JSX types
---

# JSX types

The type system that powers Vincle: `Renderable`, `VNode`, and the `JSX`
namespace.

Every HTML and SVG element is typed per element, so a typo is a compile error:

```tsx
<dvi clas="card" />
//^ error: 'dvi' does not exist
//  ^ error: 'clas' does not exist on <div>. Did you mean 'class'?
```

The types ship inside `@vincle/core`, with the attribute names and value unions
you know from React. Nothing from React is bundled or imported at runtime.

## Renderable

Everything a component may return, and everything the renderers know how to
render.

```ts
import type { RawString, VNode } from "@vincle/core";

export type Renderable =
  | VNode
  | RawString
  | string
  | number
  | bigint
  | boolean
  | null
  | undefined
  | Iterable<Renderable>
  | AsyncIterable<Renderable>
  | Promise<Renderable>;
```

| Type                             | Example                          | Behaviour                               |
| -------------------------------- | -------------------------------- | --------------------------------------- |
| `string`                         | `"a & b"`                        | HTML-escaped and rendered as text       |
| `number` / `bigint`              | `42`, `100n`                     | Rendered as text                        |
| `boolean` / `null` / `undefined` | `true`, `null`                   | Renders nothing                         |
| `RawString`                      | `raw("<b>bold</b>")`             | Passes through verbatim, no escaping    |
| `Promise<Renderable>`            | `Promise.resolve(<p>hi</p>)`     | Awaited                                 |
| `Renderable[]`                   | `[<a/>, <b/>]`                   | Rendered in order, nesting flattened    |
| `Iterable<Renderable>`           | `Set`, `Map.values()`, generator | Drained, then rendered in order         |
| `AsyncIterable<Renderable>`      | `AsyncGenerator`                 | Awaited per item, flushed between items |

Anything else, such as a plain object, a `Date`, or a `URL`, is rendered through
its string form. A function or a symbol is a type error.

## VNode

One element built by JSX: a tag, its props, its children. It is exported as a
type only: `jsx()` is the only way to construct one. Use `VNode` for a
value that holds an element; for anything that merely _renders_, use the wider
`Renderable`.

## Components

A component is a plain function, possibly `async`, returning anything in
`Renderable`. There is no `Component<P>` type to apply:

```tsx
import type { Renderable } from "@vincle/core";

const Greeting = ({ name }: { name: string }) => <h1>Hello, {name}!</h1>;

const AsyncGreeting = async ({ id }: { id: string }) => {
  const user = await db.users.find(id);
  return <h1>Hello {user.name}</h1>;
};

const Items = () => [<li>a</li>, <li>b</li>];

const Card = ({ children }: { children?: Renderable }) => <div class="card">{children}</div>;
```

To accept children, declare them as `Renderable`.

## Attributes

Attribute names use React's camelCase spelling; the engine maps each one to its
HTML name.

| You write     | The document carries |
| ------------- | -------------------- |
| `className`   | `class`              |
| `htmlFor`     | `for`                |
| `tabIndex`    | `tabindex`           |
| `httpEquiv`   | `http-equiv`         |
| `strokeWidth` | `stroke-width`       |
| `xlinkHref`   | `xlink:href`         |
| `viewBox`     | `viewBox`            |

HTML names (`class`, `style`, `data-*`, `aria-*`, `http-equiv`, `xlink:href`…)
can be written directly too.

Values may be a string, a number, a boolean, a [`raw()`](/api/core/raw) string, or
a **promise** of any of those. A boolean attribute renders as the bare name:
`<input disabled />` becomes `<input disabled>`.

```tsx
<a href={resolveUrl(slug)} class={["btn", active && "btn-active"]}>
  link
</a>
```

### Event handlers

Handlers are **strings**, not functions, since Vincle emits HTML. A function
throws at render time.

```tsx
<button onclick="submit()">Click me</button>
<button onClick="submit()">Both spellings work</button>
```

The
[`no-unsafe-event-handlers`](https://github.com/cjean-fr/vincle/tree/main/packages/eslint-plugin)
rule flags inline handlers at the source if you would rather avoid them.

## JSX namespace

Vincle declares its own `JSX` namespace, re-exported by each JSX runtime entry
point: TypeScript finds it through the `jsxImportSource` in your
`tsconfig.json`, so you rarely touch it directly.

| Member                         | Purpose                                                      |
| ------------------------------ | ------------------------------------------------------------ |
| `JSX.Element`                  | What `jsx()` produces                                        |
| `JSX.ElementType`              | What may appear as a tag: a string, or a component function  |
| `JSX.IntrinsicElements`        | Attribute types per element, plus open custom elements       |
| `JSX.IntrinsicAttributes`      | Props every element accepts without being attributes (`key`) |
| `JSX.ElementChildrenAttribute` | Names the prop JSX children are written into                 |

### Custom elements

Any name containing a hyphen is a custom element: it accepts any attribute, and
its children are checked as `Renderable`.

```tsx
<my-widget theme="dark" data-id="7">
  content
</my-widget>
```

To type one, augment `JSX.IntrinsicElements`. The props must be a **type
literal**, not a named `interface` (an interface cannot satisfy the
custom-element index signature: error TS2411):

```tsx
import type { Renderable } from "@vincle/core";

declare module "@vincle/core/jsx-runtime" {
  namespace JSX {
    interface IntrinsicElements {
      "turbo-frame": { src?: string; target?: string; children?: Renderable };
    }
  }
}
```

## CSSProperties

Style objects use camelCase properties, converted to kebab-case on output:

```tsx
import type { CSSProperties } from "@vincle/core";

const styles = {
  backgroundColor: "#fff",
  fontSize: "14px",
  "--custom-var": "value", // CSS variables supported
} satisfies CSSProperties;
```

Property _names_ are validated: one carrying `:`, `;` or `{` is dropped, so keys
coming from data cannot smuggle extra declarations into the attribute. Values are
CSS-escaped for the same reason: a `;` in a value stays inside it.

## ClassValue

What the `class` attribute accepts: a string, or a flat list whose falsy entries
are dropped.

```tsx
<div class={["card", isActive && "card-active", null]} />
// => <div class="card card-active">
```
