# Frontend stack

The default Tailwind CSS, DaisyUI and Alpine.js setup, and React, Vue or any other toolset.

Nimbly renders HTML on the server and leaves the browser side to you. A project gets Tailwind CSS, DaisyUI and Alpine.js set up by default, and the admin is built with them. Your own routes can use them, add to them, or use a different toolset altogether.

## The default stack

| Tool | Used for |
|---|---|
| Tailwind CSS 4 | Utility classes in templates |
| DaisyUI 5 | Component classes and themes |
| Alpine.js | Interaction, directly in the markup |

`./nimbly build` scans the templates of `core/` and `ext/` for classes and builds `ext/static/app.css` and `ext/static/app.js`. `./nimbly watch` keeps them current while you edit.

- **Theme.** Colors and DaisyUI themes are in `ext/tailwind.theme.js`. The admin and the Nimbly bar use the same tokens, so the editing tools follow the brand.
- **Styles.** `ext/theme.css` holds the project's own CSS, imported after the framework styles so it wins.
- **Classes in variables.** Tailwind finds classes by reading the source. Quote a class list that is stored in a variable, `[#set menu-text="text-stone-500"#]`, so the classes appear as a string in the template.

### Server-rendered markup with Alpine

Alpine adds behavior to markup the server already rendered. Keep content in HTML when the browser only shows, hides, sorts or filters it, and give Alpine the few values it needs:

```html
<article data-sector="[#item.sector#]" x-show="sector === 'all' || sector === $el.dataset.sector">
  [#get-html item.description#]
</article>
```

When the browser builds the interface from data, pass the records as JSON in a script block, which is safe for any text the records hold:

```html
<script type="application/json" id="projects-data">[#fmt var=data.projects empty={} type=json#]</script>
<div x-data="{ projects: Object.values(JSON.parse(document.getElementById('projects-data').textContent)) }">
  <template x-for="project in projects" :key="project.uuid">
    <h3 x-text="project.title"></h3>
  </template>
</div>
```

For large or private data sets, let the page ask the API for one page at a time instead of embedding everything.

## A library on one page

Load a library on the route that needs it. The projects example puts its map on one page with Leaflet:

```html
[#set head="<link rel='stylesheet' href='https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'>"#]
[#html#]
```

and loads the script with the route's own `index.js`, or a `<script>` in its `main.tpl`. Other pages never load it.

## React, Vue or another framework

A route can host an application written with any framework. The pattern is the same for each:

1. **Build** the application with its own toolchain, for example Vite, into a folder under `ext/static/`. Files in `ext/static/` are served from the site root, so `ext/static/planner/main.js` is `/planner/main.js`.
2. **Mount** it from a route: render the element it mounts on, and load the build.
3. **Pass data** in the page, or let the application call the API.

A route that keeps the standard shell, `ext/uri/planner/index.tpl`:

```html
[#set page-title="Planner"#]
[#set head="<link rel='stylesheet' href='[#base-url#]/planner/main.css'>"#]
[#html#]
```

and its `main.tpl`:

```html
[#data projects filter=published:1#]
<div id="planner"></div>
<script type="application/json" id="planner-data">[#fmt var=data.projects empty={} type=json#]</script>
<script type="module" src="[#base-url#]/planner/main.js"></script>
```

The application reads its starting data from the script block:

```js
import { createRoot } from 'react-dom/client';
import Planner from './Planner';

const projects = Object.values(JSON.parse(document.getElementById('planner-data').textContent));
createRoot(document.getElementById('planner')).render(<Planner projects={projects} />);
```

In the browser, the application can read and write through the REST API with the signed-in user's session, under the same permissions as the admin:

```js
const response = await fetch(`${window.nb.base_url}/api/v1/projects?limit=20`, { credentials: 'same-origin' });
const { projects, total } = await response.json();
```

`window.nb.base_url` is set by the shell. On a route with a document of its own, write the base address into the page with `[#base-url#]`.

Keep the application's source and toolchain in a folder of its own inside `ext/`, such as `ext/planner/` with its own `package.json`, and commit or deploy its build output the same way as `app.css`.

## A different stack for the whole site

To use another CSS framework or no framework on every public page, give each route a document of its own, or replace parts of the page shell in `ext/tpl/html/`. The admin uses the same shell and needs `app.css` and `app.js`, so add your assets next to them rather than removing them. See [Your own HTML](/en/docs/guides/html-shell/).

## Read next

- [Your own HTML](/en/docs/guides/html-shell/)
- [REST API](/en/docs/reference/api/)
- [Build](/en/docs/operate/build/)
