---
title: Add a component to your app
description: Install @markless/ui, add its ui() plugin to vite.config.ts, and write a family's parts as tags.
sidebar: { label: Using components }
---

You saw a modal as a stack of parts. Now put a family in your own app.

`@markless/ui` ships its `.tsrx` source. The Markless compiler builds it together with your own components, so the same rules apply to both.

## Install and add the plugin

```package-install
npm install @markless/ui
```

```ts vite.config.ts
import { defineConfig } from 'vite';
import { markless } from '@markless/core/vite';
import { ui } from '@markless/ui/vite';

export default defineConfig({
	plugins: [ui(), markless()],
});
```

`ui()` turns icon tags, such as `<lucide.check />`, into inline SVG. It also tells Vite to leave the package to the Markless compiler. Icon packs such as `lucide` import from `@markless/ui` too. In a multi-page app built with the router, the list is `[ui(), markless(), router()]`.

The compiler plans each family's updates before your app runs, like your own components. Then the same family renders wherever your app does: in the browser alone, on a server, or in a test. The package's own tests render each family in the browser alone and from server HTML.

Pick a place to render the select. Then open it.

<UiAnywhereFigure />

Notice that the parts and their `ui-*` attributes are the same in every place.

## Write the parts as tags

Import a family by name. Each part is a tag on that name.

```tsrx fruit-picker.tsrx
import { state } from '@markless/core';
import { select } from '@markless/ui';

export default function FruitPicker() @{
	let chosen = state('');

	<form>
		<select.root name="fruit" onChange={(value: string) => { chosen = value; }}>
			<select.label>Favorite fruit</select.label>
			<select.trigger>Choose a fruit</select.trigger>
			<select.field />
			<select.content>
				<select.item value="apple">
					<select.itemlabel>Apple</select.itemlabel>
					<select.itemindicator>Chosen</select.itemindicator>
				</select.item>
				<select.item value="cherry">
					<select.itemlabel>Cherry</select.itemlabel>
					<select.itemindicator>Chosen</select.itemindicator>
				</select.item>
			</select.content>
		</select.root>
		<p>You picked: {chosen}</p>
	</form>
}
```

`onChange` gets the new value. `select.field` is the element the form submits, under the root's `name`.

## Open a modal from your own state

A modal takes one `modal.trigger`. To open it from somewhere else, pass `open` from your state.

```tsrx session.tsrx
import { state } from '@markless/core';
import { modal } from '@markless/ui';

export default function Session() @{
	let isOpen = state(false);

	<section>
		<button type="button" onClick={() => { isOpen = true; }}>Open</button>
		<modal.root open={isOpen} onChange={(next: boolean) => { isOpen = next; }}>
			<modal.backdrop>
				<modal.content>
					<modal.title>Session expired</modal.title>
					<modal.close>Sign in again</modal.close>
				</modal.content>
			</modal.backdrop>
		</modal.root>
	</section>
}
```

When Escape or **Sign in again** closes the modal, `onChange` sets `isOpen` back to `false`.

:::danger[Why does my icon tag throw "reached runtime"?]
If `ui()` is missing from `vite.config.ts`, an icon tag such as `<lucide.check />` stays code. When the page runs, it throws this error:

```text
@markless/icons: <lucide.check /> reached runtime. Add ui() from '@markless/ui/vite' before markless() in vite.config, or icons() from '@markless/icons/vite' when you use icons without @markless/ui.
```

Add `ui()` to your plugins, before `markless()`.
:::

:::warning[Why can't npm find @markless/ui/vite?]
The `ui()` plugin is in version 0.5.0 in the Markless repository. Version 0.4.0, the latest on npm, has no `/vite` entry.
:::

**Next:** Your select works, but it looks like plain HTML. How do you give it a look? [Styling components →](/ui/styling-components)
