Skip to content
These are temporary community docs. The official docs are in progress.Help improve them
Markless
Esc
↑↓navigate↵open⌘Jpreview
On this page

Add a component to your app

Install @markless/ui, add its ui() plugin to vite.config.ts, and write a family's parts as tags.

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

npm install @markless/ui
pnpm add @markless/ui
yarn add @markless/ui
bun add @markless/ui
nub add @markless/ui
aube add @markless/ui
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.

Does a select change when it renders somewhere else?

Pick a place to render. Then open Choose a fruit and pick one.

Where to render the select
The pageTry it
Fruit pickerbuilt in the browser

You picked:

Same select from every choice.

What Markless wrote on each partSame everywhere
  • select.rootui-closed
  • select.labelno ui-* attribute
  • select.triggerui-closed
  • select.contentui-closedhidden
  • select.itemAppleno ui-* attribute
  • select.itemindicatorui-hidden
  • select.itemCherryno ui-* attribute
  • select.itemindicatorui-hidden
What Markless did
Parts that show ui-open0
Parts that differ between places0same parts, same attributes
  1. noteBefore your app ran, the compiler planned which attributes read open and which read the choice.
  2. ranBuilt the select once, in the browser.

Simplified. The look comes from a few CSS rules of our own. The parts list leaves out the item label parts and the hidden form field. The package's own tests build every family both in the browser alone and from server HTML.

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.

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.

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.

Next: Your select works, but it looks like plain HTML. How do you give it a look? Styling components →

Was this page helpful?