# Headless controls

The viewer's layer is yours to fill. Leave out the default controls and render your own caption, counter and buttons. They read the state and call the commands.

#### React

`headless.tsx`

```tsx
import { itemLabel } from "@ogdakke/fluidbox";
import { Fluidbox, useFluidbox } from "@ogdakke/fluidbox/react";
import { photos } from "./data";

function Caption() {
  const { item, index, count, hasPrevious, hasNext, previous, next, close } = useFluidbox();
  return (
    <div className="pattern-caption">
      <button type="button" disabled={!hasPrevious} onClick={() => previous()}>
        Previous
      </button>
      <span>
        <strong>{item && itemLabel(item)}</strong>{" "}
        <small>{index === null ? "" : `${index + 1} / ${count}`}</small>
      </span>
      <button type="button" disabled={!hasNext} onClick={() => next()}>
        Next
      </button>
      <button type="button" onClick={() => close()}>
        Close
      </button>
    </div>
  );
}

export function Headless() {
  return (
    <Fluidbox.Root items={photos}>
      <div className="pattern-grid">
        {photos.map((photo) => (
          <Fluidbox.Trigger
            key={photo.src}
            item={photo}
            className="pattern-thumb"
          >
            <img src={photo.src} alt={itemLabel(photo)} />
          </Fluidbox.Trigger>
        ))}
      </div>
      {/* Your own controls and caption replace the default ones. */}
      <Fluidbox.Viewer>
        <Caption />
      </Fluidbox.Viewer>
    </Fluidbox.Root>
  );
}
```

#### Vue

`Headless.vue`

```vue
<script setup lang="ts">
import { itemLabel } from "@ogdakke/fluidbox";
import { Fluidbox } from "@ogdakke/fluidbox/vue";
import { photos } from "./data";
</script>

<template>
  <Fluidbox.Root v-slot="fluidbox" :items="photos">
    <div class="pattern-grid">
      <Fluidbox.Trigger
        v-for="photo in photos"
        :key="photo.id"
        :item="photo"
        class="pattern-thumb"
      >
        <img :src="photo.src" :alt="itemLabel(photo)" />
      </Fluidbox.Trigger>
    </div>
    <!-- Your own controls and caption, in place of the default ones. -->
    <Fluidbox.Viewer>
      <div class="pattern-caption">
        <button
          type="button"
          :disabled="!fluidbox.hasPrevious"
          @click="fluidbox.previous()"
        >
          Previous
        </button>
        <span v-if="fluidbox.item && fluidbox.index !== null">
          <strong>{{ itemLabel(fluidbox.item) }}</strong>
          <small>{{ fluidbox.index + 1 }} / {{ fluidbox.count }}</small>
        </span>
        <button
          type="button"
          :disabled="!fluidbox.hasNext"
          @click="fluidbox.next()"
        >
          Next
        </button>
        <button type="button" @click="fluidbox.close()">Close</button>
      </div>
    </Fluidbox.Viewer>
  </Fluidbox.Root>
</template>
```

#### Solid

`headless.tsx`

```tsx
import { For } from "solid-js";
import { itemLabel } from "@ogdakke/fluidbox";
import { Fluidbox, useFluidbox } from "@ogdakke/fluidbox/solid";
import { photos } from "./data";

function Caption() {
  const fluidbox = useFluidbox();
  return (
    <div class="pattern-caption">
      <button
        type="button"
        disabled={!fluidbox.hasPrevious}
        onClick={() => fluidbox.previous()}
      >
        Previous
      </button>
      <span>
        <strong>{fluidbox.item ? itemLabel(fluidbox.item) : ""}</strong>{" "}
        <small>
          {(fluidbox.index ?? 0) + 1} / {fluidbox.count}
        </small>
      </span>
      <button
        type="button"
        disabled={!fluidbox.hasNext}
        onClick={() => fluidbox.next()}
      >
        Next
      </button>
      <button type="button" onClick={() => fluidbox.close()}>
        Close
      </button>
    </div>
  );
}

export function Headless() {
  return (
    <Fluidbox.Root items={photos}>
      <div class="pattern-grid">
        <For each={photos}>
          {(photo) => (
            <Fluidbox.Trigger item={photo} class="pattern-thumb">
              <img src={photo.src} alt={itemLabel(photo)} />
            </Fluidbox.Trigger>
          )}
        </For>
      </div>
      {/* Your own controls and caption, in place of the default controls. */}
      <Fluidbox.Viewer>
        <Caption />
      </Fluidbox.Viewer>
    </Fluidbox.Root>
  );
}
```

#### Angular

`headless.ts`

```ts
import { Component } from "@angular/core";
import { itemLabel } from "@ogdakke/fluidbox";
import { FluidboxModule, injectFluidbox } from "@ogdakke/fluidbox/angular";
import { photos } from "./data";

/** Your own controls and caption. In the viewer, it injects the fluidbox of the root. */
@Component({
  selector: "div[appCaption]",
  standalone: true,
  host: { class: "pattern-caption" },
  template: `
    <button
      type="button"
      [disabled]="!fluidbox().hasPrevious"
      (click)="fluidbox().previous()"
    >
      Previous
    </button>
    <span>
      <strong>{{ title() }}</strong>
      <small>{{ (fluidbox().index ?? 0) + 1 }} / {{ fluidbox().count }}</small>
    </span>
    <button
      type="button"
      [disabled]="!fluidbox().hasNext"
      (click)="fluidbox().next()"
    >
      Next
    </button>
    <button type="button" (click)="fluidbox().close()">Close</button>
  `,
})
export class Caption {
  readonly fluidbox = injectFluidbox();
  readonly title = injectFluidbox(({ item }) => (item ? itemLabel(item) : ""));
}

@Component({
  selector: "pattern-headless",
  standalone: true,
  imports: [FluidboxModule, Caption],
  template: `
    <div fluidboxRoot [items]="photos" class="pattern-grid">
      @for (photo of photos; track photo.id) {
        <button [fluidboxTrigger]="photo" class="pattern-thumb">
          <img [src]="photo.src" [alt]="label(photo)" />
        </button>
      }
      <!-- A viewer template replaces the default controls. -->
      <ng-template fluidboxViewer>
        <div appCaption></div>
      </ng-template>
    </div>
  `,
})
export class Headless {
  readonly photos = photos;
  readonly label = itemLabel;
}
```

#### Elements

`headless.html`

```html
<fluidbox-gallery class="pattern-grid">
  <fluidbox-item item-id="photo-aurora">
    <button slot="trigger" type="button" class="pattern-thumb">
      <img src="/art/aurora.svg" alt="Aurora over the tundra" />
    </button>
    <template slot="content">
      <img src="/art/aurora.svg" alt="Aurora over the tundra" width="1100" height="1650" />
    </template>
  </fluidbox-item>
  <fluidbox-item item-id="photo-ridge">
    <button slot="trigger" type="button" class="pattern-thumb">
      <img src="/art/ridge.svg" alt="Night ridge" />
    </button>
    <template slot="content">
      <img src="/art/ridge.svg" alt="Night ridge" width="1200" height="1500" />
    </template>
  </fluidbox-item>
  <fluidbox-item item-id="photo-fjord">
    <button slot="trigger" type="button" class="pattern-thumb">
      <img src="/art/fjord.svg" alt="Fjord in the blue hour" />
    </button>
    <template slot="content">
      <img src="/art/fjord.svg" alt="Fjord in the blue hour" width="1200" height="1600" />
    </template>
  </fluidbox-item>
  <fluidbox-item item-id="photo-lagoon">
    <button slot="trigger" type="button" class="pattern-thumb">
      <img src="/art/lagoon.svg" alt="Lagoon after sunset" />
    </button>
    <template slot="content">
      <img src="/art/lagoon.svg" alt="Lagoon after sunset" width="1800" height="1200" />
    </template>
  </fluidbox-item>
</fluidbox-gallery>
<template>
  <div class="pattern-caption">
    <span>
      <strong></strong>
      <small></small>
    </span>
    <button type="button" data-action="previous">Previous</button>
    <button type="button" data-action="next">Next</button>
    <button type="button" data-action="close">Close</button>
  </div>
</template>
```

`headless.ts`

```ts
import { itemLabel } from "@ogdakke/fluidbox";

export function headless(root: HTMLElement) {
  const gallery = root.querySelector("fluidbox-gallery")!;
  const caption = root.querySelector<HTMLTemplateElement>(":scope > template")!;
  gallery.viewer = ({ target, fluidbox, signal }) => {
    target.append(caption.content.cloneNode(true));
    const action = (name: string) =>
      target.querySelector<HTMLButtonElement>(`[data-action="${name}"]`)!;
    const previous = action("previous");
    const next = action("next");
    previous.addEventListener("click", () => fluidbox.previous(), { signal });
    next.addEventListener("click", () => fluidbox.next(), { signal });
    action("close").addEventListener("click", () => fluidbox.close(), { signal });
    const title = target.querySelector("strong")!;
    const counter = target.querySelector("small")!;
    return fluidbox.subscribe(({ item, index, count, hasPrevious, hasNext }) => {
      if (!item || index === null) return;
      title.textContent = itemLabel(item);
      counter.textContent = `${index + 1} / ${count}`;
      previous.disabled = !hasPrevious;
      next.disabled = !hasNext;
    });
  };
}
```

#### JavaScript

`headless.ts`

```ts
import { createFluidbox, itemLabel } from "@ogdakke/fluidbox";
import { photos } from "./data";

export function mountHeadless(root: HTMLElement) {
  const fluidbox = createFluidbox({
    items: photos,
    // Your own controls and caption, drawn into the viewer while it is open.
    viewer({ target, fluidbox, signal }) {
      const caption = document.createElement("div");
      caption.className = "pattern-caption";
      caption.innerHTML = `
        <button type="button" data-action="previous">Previous</button>
        <span><strong></strong> <small></small></span>
        <button type="button" data-action="next">Next</button>
        <button type="button" data-action="close">Close</button>`;
      const action = (name: string) =>
        caption.querySelector<HTMLButtonElement>(`[data-action="${name}"]`)!;
      const prev = action("previous");
      const next = action("next");
      const title = caption.querySelector("strong")!;
      const counter = caption.querySelector("small")!;
      prev.addEventListener("click", () => void fluidbox.previous(), { signal });
      next.addEventListener("click", () => void fluidbox.next(), { signal });
      action("close").addEventListener("click", () => void fluidbox.close(), { signal });
      target.append(caption);

      return fluidbox.subscribe(({ item, index, count, hasPrevious, hasNext }) => {
        if (!item || index === null) return;
        title.textContent = itemLabel(item);
        counter.textContent = `${index + 1} / ${count}`;
        prev.disabled = !hasPrevious;
        next.disabled = !hasNext;
      });
    },
  });

  const grid = document.createElement("div");
  grid.className = "pattern-grid";
  photos.forEach((photo, index) => {
    const button = document.createElement("button");
    button.type = "button";
    button.className = "pattern-thumb";
    button.append(Object.assign(new Image(), { src: photo.src, alt: itemLabel(photo) }));
    grid.append(button);
    fluidbox.bind(button, index);
  });
  root.append(grid);

  return () => {
    fluidbox.destroy();
    grid.remove();
  };
}
```

- The framework `Viewer` part renders into the viewer's layer, above the slides, and `useFluidbox()` gives the state and commands. In plain JavaScript, the `viewer` option draws into it and subscribes to the state. On custom elements, set `gallery.viewer`, or put a `<template slot="viewer">` in the gallery.
- Buttons marked `data-fluidbox-close`, `data-fluidbox-previous` or `data-fluidbox-next` get the default styling, and run their command on the custom elements. Your own buttons call `close()`, `previous()` and `next()` directly.
- Clicks on your buttons, links and inputs inside the viewer never count as zoom taps.
