# `hb-offcanvas` — integrator guide

**Category:** layout · **Tags:** layout, navigation · **Package:** `@htmlbricks/hb-offcanvas`

## Summary

`hb-offcanvas` is a fixed, slide-in drawer on the start edge of the viewport that embeds **`hb-sidebar-desktop`** for branded navigation, page selection, optional theme switching, and optional language switching. When **`type`** is **`autohide`**, a full-viewport Bulma **`modal-background`** layer sits under the panel; clicking the dimmed area closes the drawer. The host raises custom events when the drawer opens or closes, when the active page changes, and when the nested sidebar changes language or theme.

The package depends on **`hb-sidebar-desktop`** (which in turn uses **`hb-sidenav-link`**). Navigation items use the same JSON shape as the sidebar, including optional Bootstrap Icons glyph names on each link.

## Behavior

- **`type`**
  - **`autohide`** (default): Renders the backdrop while the drawer is open. Clicking the backdrop calls the internal close path and dispatches **`offcanvasswitch`** with **`isOpen: false`**.
  - **`open`**: After the host’s reactive setup runs, the drawer is treated as **always open** (the **`opened`** attribute is forced to an open state for display).
  - **`small`**: No backdrop; open state follows **`opened`** like a non-`open` mode. Use when you want a sliding panel without modal dismissal (same stacking and panel animation as other modes, without the overlay).

- **`opened`**: Initial and runtime open state for modes that are not **`type="open"`**. In HTML, use string booleans **`yes`** / **`no`** as for other web components in this library. The implementation treats a truthy **`opened`** that is not the string **`no`** as open.

- **Page changes**: Choosing a nav entry inside **`hb-sidebar-desktop`** runs the sidebar’s page handler; the host re-dispatches **`pageChange`** with **`{ page: string }`** and closes the drawer via **`OpenSwitch(false)`** (so the drawer does not stay open after navigation in the default wiring).

- **`groups`**: May be a parsed JSON string in the effect; invalid JSON is ignored and leaves the previous value.

## Styling (Bulma and host tokens)

The shadow tree uses Bulma-derived tokens for the panel fill, shadow, and (when **`autohide`**) backdrop color. Stacking and drawer width are controlled on **`:host`** so the drawer can sit above siblings such as a mobile layout shell. See [Bulma CSS variables](https://bulma.io/documentation/features/css-variables/) and `extra/docs.ts` for metadata.

| Variable | Purpose |
|----------|---------|
| `--hb-offcanvas-host-z` | Host stacking context so fixed panel and backdrop paint above following light-DOM siblings (default `1038`). |
| `--hb-offcanvas-panel-z` | Drawer layer above the backdrop (default `1045`). |
| `--hb-offcanvas-backdrop-z` | Backdrop layer (default `1040`). |
| `--hb-offcanvas-panel-width` | Drawer width, capped by the viewport (default `240px`). |
| `--bulma-scheme-main` | Panel background. |
| `--bulma-shadow` | Elevation shadow on the open drawer. |
| `--bulma-modal-background-background-color` | Backdrop tint when **`type`** is **`autohide`**. |

The component also loads **Bootstrap Icons** from jsDelivr for icon font classes used by nested navigation.

## CSS parts

None on **`hb-offcanvas`**. **`hb-sidebar-desktop`** defines its own parts (for example the header row); see the sidebar README for **`::part`** names.

## HTML slots

| Slot | Purpose |
|------|---------|
| `test` | Optional content at the host root inside **`#webcomponent`**, before the panel (for advanced integrations). Main navigation UI lives inside the nested **`hb-sidebar-desktop`**, not in this slot. |

## Custom element tag

```html
<hb-offcanvas …></hb-offcanvas>
```

## Attributes (snake_case; string values in HTML)

Reflectable props follow the usual conventions: booleans as **`yes`** / **`no`**, objects and arrays as JSON strings, numbers as decimal strings where applicable.

| Attribute | Required | Description |
|-----------|----------|-------------|
| `id` | No | Optional element id. |
| `style` | No | Optional inline host styles. |
| `type` | No | **`autohide`** \| **`open`** \| **`small`** — backdrop and forced-open behavior (see above). Default in implementation: **`autohide`**. |
| `opened` | No | **`yes`** / **`no`** — whether the drawer is open (ignored for forced-open when **`type`** is **`open`**). |
| `navlinks` | No | JSON string — **`INavLink[]`**: **`key`**, **`label`**, optional **`icon`** (Bootstrap Icons name), **`group`**, **`badge`**, **`subLinks`**, etc. Same contract as **`hb-sidebar-desktop`**. |
| `navpage` | No | Active page **`key`** string. |
| `groups` | No | JSON string — **`{ key: string; label: string }[]`** for section labels in the sidebar. |
| `companylogouri` | No | Logo image URI for the sidebar header. |
| `companytitle` | No | Company or product title next to the logo. |
| `enablefooter` | No | **`yes`** / **`no`** / **`false`** (same contract as **`hb-sidebar-desktop`**). Forwarded to the nested sidebar; omitted or truthy values keep the sidebar footer enabled (**`yes`**). |
| `enablethemeswitch` | No | **`yes`** / **`no`** — passed through to **`hb-sidebar-desktop`** (defaults to **`yes`** when unset). |
| `themepreference` | No | **`light`** \| **`dark`** \| **`auto`** — optional controlled theme for the nested sidebar. |
| `i18nlang` | No | Language code for the nested sidebar. |
| `i18nlanguages` | No | JSON string or structured options — **`{ code: string; label: string }[]`** for the language picker. |

## Events

| Event | Detail |
|-------|--------|
| `offcanvasswitch` | `{ isOpen: boolean }` — emitted when the drawer opens or closes (including initial open when **`type`** is **`open`** or **`opened`** is truthy, and when the backdrop closes the drawer). |
| `pageChange` | `{ page: string }` — forwarded from **`hb-sidebar-desktop`** when the user selects a page; the offcanvas also closes after dispatching. |
| `languageChange` | `{ code: string }` — bubbled from the nested sidebar’s language control. |
| `themeChange` | `{ mode: ThemePreference }` where **`ThemePreference`** is **`"light"` \| `"dark"` \| `"auto"`** — bubbled from the nested sidebar’s theme control. |

## Usage notes

- **Icons:** Use Bootstrap Icons **names** only (for example **`house-door`**), not class strings — same rule as **`hb-sidebar-desktop`**.
- **Layering:** If the drawer or backdrop appears under a sibling layout, raise **`--hb-offcanvas-host-z`** (and panel/backdrop z-tokens if needed) on the host.
- **Examples:** Storybook-style **`examples`** in `extra/docs.ts` cover default, closed, autohide, and grouped navigation with footer metadata on the data model.

## TypeScript typings (authoring)

From `types/webcomponent.type.d.ts`:

```typescript
import type { INavLink } from "../../sidenav-link/types/webcomponent.type";
import type {
  I18nLanguageOption,
  ThemePreference,
} from "../../sidebar-desktop/types/webcomponent.type";

export type Component = {
  id?: string;
  style?: string;
  opened?: boolean;
  navlinks?: INavLink[];
  navpage?: string;
  groups?: { key: string; label: string }[];
  companylogouri?: string;
  companytitle?: string;
  enablefooter?: "yes" | "no" | "false" | null | "" | undefined;
  enablethemeswitch?: "yes" | "no" | null | "" | undefined;
  themepreference?: ThemePreference | null | "" | undefined;
  i18nlang?: string | null | "" | undefined;
  i18nlanguages?: I18nLanguageOption[] | string | null | "" | undefined;
  type?: "open" | "autohide" | "small";
};

export type Events = {
  offcanvasswitch: { isOpen: boolean };
  pageChange: { page: string };
  languageChange: { code: string };
  themeChange: { mode: ThemePreference };
};
```

## Minimal HTML

```html
<hb-offcanvas
  opened="yes"
  type="autohide"
  companytitle="Acme"
  navpage="home"
  navlinks='[{"label":"Home","key":"home","icon":"house-door"}]'
></hb-offcanvas>
```

Listen for open state and navigation:

```html
<script>
  const el = document.querySelector("hb-offcanvas");
  el.addEventListener("offcanvasswitch", (e) => {
    console.log("open:", e.detail.isOpen);
  });
  el.addEventListener("pageChange", (e) => {
    console.log("page:", e.detail.page);
  });
</script>
```
