# Styling

The viewer has no class names. You style it through data attributes and CSS custom properties. The package's own rules have as little weight as they can, so a rule of yours with the same selector wins. Every hook is listed in the [CSS reference](https://fluidbox.dwe.workers.dev/docs/css-reference.md).

## Order

Import the package CSS first, then yours:

```ts
import "@ogdakke/fluidbox/styles.css";
import "./gallery-theme.css";
```

The package wraps the viewer's rules in `:where([data-fluidbox-overlay], [data-fluidbox-flight])`, which adds nothing to their specificity. A later rule on the same part overrides the default:

```css
[data-fluidbox-close] {
  width: 40px;
  height: 40px;
  border-radius: 8px;
}
```

The package declares the filmstrip's and the sheet's variables on the dialog with no weight at all, so set them on `[data-fluidbox-dialog]`:

```css
[data-fluidbox-dialog] {
  --fluidbox-filmstrip-item-size: 64px;
  --fluidbox-sheet-peek: 120px;
}
```

## The viewer layer

While it is open, the viewer is one element, `[data-fluidbox-overlay]`, at the end of `<body>`. It is not inside your component, so it doesn't inherit your component's styles or CSS variables. Inside it:

| Selector | Part |
| --- | --- |
| `[data-fluidbox-overlay]` | The viewer's root, at the end of `<body>` while open. |
| `[data-fluidbox-backdrop]` | The backdrop. A click on it closes the viewer. |
| `[data-fluidbox-container]` | The viewport-sized box at the page's scroll position. |
| `[data-fluidbox-dialog]` | The dialog, with `role="dialog"` and `aria-modal="true"`. It carries the state attributes. |
| `[data-fluidbox-slides]` | The horizontal scroller of slides. |
| `[data-fluidbox-slide]` | A slide. `aria-hidden="false"` on the current one. |
| `[data-fluidbox-frame]` | The box that crops and rounds the media. |
| `[data-fluidbox-media]` | The `<img>`, `<video>`, `<iframe>` or custom content element. |
| `[data-fluidbox-portal]` | The viewer's layer, where your controls and captions go. |
| `[data-fluidbox-addons]` | Beside the viewer's layer. The filmstrip goes here, so the layer holds only your content. It takes no box. |

While the viewer is open, every other child of `<body>` is `inert`. The overlay's z-index is `var(--layer-fluidbox, 7)` and the flight's is `var(--layer-fluidbox-flight, 8)`. Set both on `:root` if your page has higher layers.

The container is `position: absolute` at the page's scroll position. Don't make it `fixed` or `sticky`, because iOS Safari colours its bars from a fixed or sticky layer at the screen's edge.

## Theme

When the viewer opens, it reads the trigger's computed styles and copies every `--fluidbox-*` property onto the overlay. Set them on `:root`, on a section, or on one trigger, and the viewer uses what that trigger inherits:

| Variable | Default | Used for |
| --- | --- | --- |
| `--fluidbox-background` | `#111` | The backdrop (through `--fluidbox-backdrop-background`), the close button, error messages. |
| `--fluidbox-color` | `white` | Text and icons. |
| `--fluidbox-control-background` | `rgb(20 20 20 / 0.72)` | The Previous and Next buttons; the close button on hover. |
| `--fluidbox-focus-color` | `currentColor` | Focus rings on the controls, filmstrip items, the dialog and `<fluidbox-item>` triggers. |
| `--fluidbox-filmstrip-item-radius` | `3px` | The corners of filmstrip thumbnails. |

```css
.gallery {
  --fluidbox-background: #0a0a0a;
  --fluidbox-color: #ededee;
  --fluidbox-control-background: #232326;
  --fluidbox-backdrop-background: rgb(10 10 10 / 0.72);
  --fluidbox-backdrop-filter: blur(24px) saturate(1.5);
  --fluidbox-slide-gap: 24px;
}
```

`--fluidbox-backdrop-background` defaults to `--fluidbox-background`. `--fluidbox-backdrop-filter` is `none` by default. Pair a blur with a see-through background.

## State attributes

Attribute names follow one rule. Parts of the viewer, and the markers you put on your own elements, are `data-fluidbox-*`, such as `[data-fluidbox-dialog]` and `data-fluidbox-origin`. State on the viewer's own elements is a plain flag, such as `data-open` or `data-zoomed`. State on your elements, a trigger or a thumbnail overlay, is `data-fluidbox-*`, so it never clashes with your own attributes. `data-fluidbox-status` is the one name on both, since the active trigger mirrors the viewer.

The overlay and the dialog both carry the viewer's state:

| Attribute | On | Meaning |
| --- | --- | --- |
| `data-fluidbox-status` | Overlay, dialog | `closed`, `opening`, `open` or `closing`. |
| `data-open`, `data-closed` | Overlay, dialog | Not closed, or closed. |
| `data-starting-style`, `data-opening` | Overlay, dialog | Opening. |
| `data-ending-style` | Overlay, dialog | Closing. |
| `data-dragging` | Overlay, dialog | A drag to dismiss is under way. |
| `data-dismissing` | Overlay, dialog | Closing, or a drag to dismiss is under way. |

The dialog alone also has `data-zoomed` while an image is zoomed, and `data-gallery` when there is more than one item. Style a dialog that holds a filmstrip with `[data-fluidbox-dialog]:has([data-fluidbox-filmstrip])`.

```css
[data-fluidbox-dialog][data-zoomed] .caption {
  opacity: 0;
}
```

The trigger of the open item gets `data-fluidbox-status="open"`, `data-open` and `aria-expanded="true"`. Every trigger has `data-fluidbox-trigger`, `aria-haspopup="dialog"` and `aria-expanded`.

## Controls

The default controls are buttons marked `data-fluidbox-previous`, `data-fluidbox-next` and `data-fluidbox-close`, each holding an SVG marked `data-fluidbox-icon`. Close sits in the top-right corner. With more than one item, Previous and Next sit at the middle of each side. All three keep 16 pixels inside the stage. A disabled Previous or Next fades to `--fluidbox-gallery-control-opacity: 0.35`.

You choose which controls exist:

- **Plain JavaScript.** You get all three unless you pass `viewer`. Then `controls: true`, or a list such as `controls: ["close"]`, keeps some next to your own content.
- **Frameworks.** Put the `Controls` part, or `Previous`, `Next` and `Close`, in the `Viewer` part. Without them there are no controls. Each renders the default icon and label when it is empty, or your children.
- **Custom elements.** The gallery adds all three. A `<template slot="viewer">` replaces them. Put `<fluidbox-controls>` in it to keep them. Any button marked `data-fluidbox-close`, `-previous` or `-next` in the viewer runs that command, in every API.

The button names come from the `close`, `previous` and `next` [labels](https://fluidbox.dwe.workers.dev/docs/api.md#labels).

### Your own controls

You can render anything into the viewer's layer, such as a caption bar, a counter or a share button. Read the state and call the commands. [Headless controls](https://fluidbox.dwe.workers.dev/docs/patterns/headless.md) has a full caption bar in every framework.

Everything you put in the layer behaves like the default controls: hidden during the opening flight, shown as it lands, faded out as soon as a close or a drag starts, and back if the drag is cancelled. Mark a layer outside it `data-fluidbox-chrome` to get the same. If you give a chrome layer your own `transition`, include `opacity`.

### The stage

The stage is the part of the dialog the media fits into. By default it is all of it. Inset it with `--fluidbox-stage-top`, `--fluidbox-stage-right`, `--fluidbox-stage-bottom` and `--fluidbox-stage-left` on the dialog, to make room for a sidebar or an action row. The media centres in the stage, flights land there, and the default controls keep to its edges. The insets are registered lengths, so they can transition.

```css
[data-fluidbox-dialog]:has(.replies) {
  --fluidbox-stage-right: 360px;
}
```

A zoomed image still pans under the insets, so a see-through bar floats over it. `--fluidbox-zoom-clip` on the dialog keeps it inside them: `stage` for every side, or a list of sides such as `right` or `top bottom`. To place something against the picture, read `--fluidbox-media-width` and `--fluidbox-media-height` on the dialog. The picture is centred in the stage.

## Thumbnail overlays

A badge, a play icon or a count on a thumbnail can follow the flight. Make it a thumbnail overlay: `fluidbox.bindOverlay()` in plain JavaScript, the `ThumbnailOverlay` part in the frameworks, `data-fluidbox-thumbnail-overlay` inside a `<fluidbox-item>` trigger.

- By default the overlay stays on the thumbnail.
- With `travel`, it flies with the photo and fades as the flight lands. What flies is a copy of its content, or live content that a renderer draws into the flying frame. On the custom elements, `data-fluidbox-thumbnail-overlay="travel"`.

The runtime sets `--fluidbox-closed` on the overlay, from `1` at the thumbnail to `0` fully open, and `data-fluidbox-phase` to `idle`, `opening`, `open`, `dragging` or `closing`:

```css
.photo-count {
  opacity: var(--fluidbox-closed, 1);
}
```

While an item is open, its thumbnail image on the page gets `data-fluidbox-thumbnail-hidden`, so it doesn't show twice.

## Reduced motion

With `prefers-reduced-motion: reduce`, the viewer opens and closes without flights, moves between slides without smooth scrolling, zooms and moves the sheet without animation, and its controls and filmstrip appear without fading. You don't need to set anything. Do the same for your own chrome:

```css
@media (prefers-reduced-motion: reduce) {
  .caption {
    transition: none;
  }
}
```

## Triggers

`<fluidbox-item>` triggers get a reset: no border, background or padding, `display: block`, `cursor: zoom-in`. The package wraps the reset in `:where()`, so your classes win. The framework triggers render a plain `<button type="button">` with no styles, or decorate your own element, and `fluidbox.bind()` leaves your element's styles alone.
