---
area: "Learn"
parent: "Scripting chapter"
url: "https://unpoly.com/learn/islands"
---
<nav aria-label="Unpoly docs">[All docs](https://unpoly.com/index.md) · [Scripting chapter](https://unpoly.com/learn/scripting.md)</nav>

# Framework islands

Some widgets need heavy client-side interaction, like a rich text editor or an interactive chart. You can mount such components from a frontend framework like **React** or **Vue** as _islands_: self-contained, client-rendered widgets inside a server-rendered page.

[Compilers](https://unpoly.com/learn/enhancing-elements.md) give each island a managed lifecycle: the component mounts when its element enters the page, receives props from server data, and unmounts when Unpoly swaps the fragment around it. The rest of the page needs no framework code.

<nav aria-label="Contents">

#### Contents

- [Mounting an island](#mounting)
- [Reaching out of the island](#page-interaction)
- [Keeping an island's state through updates](#keeping)
- [An island owns its subtree](#boundaries)

</nav>

## Mounting an island {#mounting}

The server renders a placeholder element, with initial props serialized into an [`[up-data]`](https://unpoly.com/learn/data.md) attribute:

```html
<div class="color-picker" up-data="{ value: '#a2d8ff' }"></div>
```

A compiler mounts a React component into every matching element. By returning a [destructor function](https://unpoly.com/learn/enhancing-elements.md#destructor), it also unmounts the component when the surrounding fragment is swapped or its overlay closes:

```js
import { createRoot } from 'react-dom/client'
import ColorPicker from './components/color-picker'

up.compiler('.color-picker', function(element, data) {
  let root = createRoot(element)
  root.render(<ColorPicker value={data.value}/>)
  return () => root.unmount()
})
```

A Vue island works the same way:

```js
import { createApp } from 'vue'
import ColorPicker from './components/ColorPicker.vue'

up.compiler('.color-picker', function(element, data) {
  let app = createApp(ColorPicker, data) // data becomes props
  app.mount(element)
  return () => app.unmount()
})
```

Simple string props can also be passed as HTML5 [data attributes](https://unpoly.com/learn/data.md#data-attributes):

```html
<div class="color-picker" data-value="#a2d8ff"></div>
```

They appear in the same [`data`](https://unpoly.com/learn/data.md) argument, so the compilers above work without changes.

Because islands mount through the regular compiler mechanism, they work on any screen: in the initial page, in updated fragments, or in [overlays](https://unpoly.com/api/up.layer.md).

## Reaching out of the island {#page-interaction}

A common pattern is an island that edits a form value. Give the island a hidden field to maintain, and the surrounding form submits like any other Unpoly form:

```html
<form method="post" action="/profile" up-submit>
  <input type="hidden" name="theme-color" value="#a2d8ff">
  <div class="color-picker" up-data="{ field: 'theme-color' }"></div>
  <button>Save</button>
</form>
```

```js
up.compiler('.color-picker', function(element, data) {
  let field = element.closest('form').elements[data.field]
  let root = createRoot(element)
  root.render(<ColorPicker value={field.value} onChange={(value) => field.value = value}/>)
  return () => root.unmount()
})
```

Shadowing a hidden field is only one of several form-association patterns Unpoly supports. See [Custom form fields](https://unpoly.com/learn/custom-form-fields.md) for the alternatives and their trade-offs.

Component callbacks can also use Unpoly's JavaScript API directly. For example, a row in a client-rendered table could open details in an overlay:

```js
<tr onClick={() => up.layer.open({ url: `/orders/${order.id}` })}>
  <td>{order.date}</td>
  <td>{order.total}</td>
</tr>
```

## Keeping an island's state through updates {#keeping}

When a fragment containing an island is updated, the destructor unmounts the old component, and the compiler mounts a new one with fresh props from the server HTML. Any client-side state inside the island is reset by this.

To keep an island while the fragment around it is updated, assign it an [`[up-keep]`](https://unpoly.com/api/up-keep.md) attribute:

```html
<div id="theme-color" class="color-picker" up-data="{ value: '#a2d8ff' }" up-keep></div>
```

When new content contains a matching element, the existing element remains attached in its current position, keeping the mounted component and all its state. Elements are matched by their [derived target](https://unpoly.com/learn/target-derivation.md), here `#theme-color`. A matching element in the new HTML counts, even when its [`[up-data]`](https://unpoly.com/api/up-data.md) differs.

A kept element ignores the new HTML, including changed [`[up-data]`](https://unpoly.com/api/up-data.md). The recommended pattern is to listen to [`up:fragment:keep`](https://unpoly.com/api/up:fragment:keep.md) and pass the new props into the already-mounted component. The event's `event.newData` property carries the parsed [data](https://unpoly.com/learn/data.md) of the new element:

```js
up.compiler('.color-picker', function(element, data) {
  let root = createRoot(element)
  let render = (data) => root.render(<ColorPicker value={data.value}/>)
  render(data)
  element.addEventListener('up:fragment:keep', (event) => render(event.newData))
  return () => root.unmount()
})
```

This updates the island in place, without remounting. Component state like cursor positions or open popovers survives the fragment update.

A simpler alternative is [`[up-keep="same-data"]`](https://unpoly.com/learn/keeping-elements.md#same-data). It keeps the island while its data is unchanged, but _remounts_ it with fresh props when the server sends changed [`[up-data]`](https://unpoly.com/api/up-data.md). Any state inside the component is reset by the remount.

See [Keeping elements](https://unpoly.com/learn/keeping-elements.md) for how elements are matched, and for ways to control what is kept.

## An island owns its subtree {#boundaries}

Unpoly and the framework each manage their own side of the island's root element:

- Elements rendered by the framework are not [compiled](https://unpoly.com/learn/enhancing-elements.md). Attributes that need a compiler, like [`[up-preload]`](https://unpoly.com/api/up-preload.md) or the selectors of your own compilers, have no effect inside an island. Links and forms are handled through events and still work; component code can also call functions like [`up.layer.open()`](https://unpoly.com/api/up.layer.open.md) directly, as shown [above](#page-interaction). Don't work around this by calling [`up.hello()`](https://unpoly.com/api/up.hello.md) on framework-rendered DOM: the framework's next reconciliation will clobber or duplicate any changes a compiler made.
- Conversely, don't [target](https://unpoly.com/learn/targeting-fragments.md) elements inside an island. The framework expects to own that DOM. Update an island through its props or state, or re-render the entire island element.

<nav aria-label="Reading order">

- [Previous: Templates](https://unpoly.com/learn/templates.md)
- [Next: Migrating legacy JavaScripts](https://unpoly.com/learn/legacy-scripts.md)

</nav>

