# OhMyEtch Docs > Atomic, non-opinionated components for WordPress. This file contains all documentation content in a single document following the llmstxt.org standard. ## OhMyEtch OhMyEtch brings **atomic, headless-style components** to WordPress. Each component owns one job — accessible behavior, interaction logic, and the structural hooks you need — and nothing else. You compose them into any layout, any visual design, and any product surface you want. That is the whole philosophy: **logic without opinion**. ## Atomic by design Most WordPress component libraries ship finished UI patterns — cards, heroes, pricing tables, checkout shells — and ask you to adapt your design to theirs. OhMyEtch goes the other way. An Accordion is not a FAQ block. A Dialog is not a modal popup template. A CartItemTitle is not a cart row layout. They are **atoms**: small, focused units that do one thing well and stay out of your way. | What the component provides | What you provide | |---|---| | Keyboard navigation, ARIA, focus management | Typography, spacing, color | | Open/close state, form scope, facet wiring | Grid, flex, breakpoints, brand | | Consistent runtime behavior across the site | Markup structure inside slots, copy, imagery | Because each atom has a narrow responsibility, the same component family can become wildly different experiences: - **Accordion** → FAQ, settings panel, sidebar nav, product specs - **Tabs** → product details, account settings, documentation sections - **Dialog + Trigger** → newsletter signup, image lightbox entry, mobile menu - **FacetTarget + SearchFacet** → job board filters, product catalog, map search - **AddToCartForm + AttributeSelector** → variable product page, quick-view drawer, archive loop card The component does not know your use case. It only guarantees the behavior works correctly every time. ## Composition is the product OhMyEtch is built for **composition**, not configuration hacks. Components nest inside each other. Triggers connect to targets by ID. Facets refresh targets. Woo atoms loop inside cart containers. You assemble molecules from atoms — exactly the structure your design needs, not the structure a plugin author guessed you would want. That matters because real projects are diverse. A marketing site, a Woo store, and a filtered archive do not share one layout — but they can share the same **behavior layer**. OhMyEtch keeps that layer consistent while your Etch patterns, ACSS classes, and HTML stay entirely yours. ## Not opinionated — on purpose Opinionated components save time until they do not. The moment you need a different markup order, a non-standard breakpoint, or a brand-specific interaction, you are fighting the block. OhMyEtch deliberately avoids that: - **No baked-in layouts** — slots and structure are yours to author - **No visual theme** — default styling is minimal; design tokens and classes are yours - **No product assumptions** — the same cart atom works in a drawer, a page, or a mini-cart dropdown - **No dead ends** — when requirements change, you rearrange composition instead of replacing the component This is what **headless** means in a WordPress context: the component runtime carries logic and accessibility; the presentation layer stays fully under your control. ## Why atomic wins in WordPress WordPress sites are long-lived. Teams change. Designs evolve. Integrations multiply. Atomic components stay stable because their contract is behavior, not appearance. When you refactor a pattern library, you restyle — you do not rewrite accordion keyboard handling. When you add WooCommerce, you compose new atoms into existing layouts instead of swapping entire page templates. You get: - **Predictable accessibility** — the same patterns everywhere, tested once - **Reuse without duplication** — one Dialog family, many modal experiences - **Freedom to scale** — from a single FAQ to a full checkout without changing philosophy - **A system that grows with you** — new atoms join the same composition model ## What is in these docs This site is the reference for composing OhMyEtch on real projects: - **[UI Components](/category/ui-components)** — Accordion, Dialog, Tabs, Carousel, and more. Accessible, keyboard-navigable, composable families. - **[Facets](/facets/overview)** — Search, filter, pagination, and map facets wired to dynamic result targets. - **[Woo Components](/woo/overview)** — Cart, checkout, and product atoms for WooCommerce. Pick a family, read how it composes, and build the layout you actually want. **Building with an AI agent?** See **[LLM & agent access](/llms)** for `llms.txt`, section bundles, and every plain Markdown export. --- ## Accordion ## Overview Use the Accordion family when you need a group of expandable sections with built-in keyboard support, ARIA wiring, and animated panel open and close behavior. The family is composed, not standalone — the root component manages shared state and keyboard navigation, while child components define each individual section. Accordion works well for FAQs, settings panels, sidebar navigation, product feature lists, and any content that benefits from progressive disclosure. ## Authoring Structure ``` Accordion └── Accordion Item ├── Accordion Header │ └── Accordion Trigger └── Accordion Content ``` ### Placement Rules | Component | Placement | Role | |---|---|---| | **Accordion** | Top-level wrapper. | Owns state, keyboard navigation, and shared options. | | **Accordion Item** | Direct child of `Accordion`. | The runtime looks for direct item children when it initializes. | | **Accordion Header** | Inside `Accordion Item`. | Provides heading semantics (`` – ``) for the trigger. | | **Accordion Trigger** | Usually inside `Accordion Header`. | Interactive control users click or focus to toggle a panel. | | **Accordion Content** | Inside `Accordion Item`, after the header. | The panel that opens and closes. | ## Quick Start {` {#slot default} {#slot default} {#slot default}Manage your account preferences here.{/slot} {/slot} {/slot} `} When the `AccordionHeader` has an empty slot and a `content.label` value, it auto-generates a trigger for you — no need to nest `AccordionTrigger` manually. {` {#slot default} {#slot default} {#slot default} {#slot default}Shipping{/slot} {/slot} {#slot default} Shipping content goes here. {/slot} {/slot} {/slot} `} Use the explicit structure when you need custom markup inside the trigger — icons, badges, or any content beyond a plain label. --- ## Family Components ### Accordion (Root) The root component wraps all items and manages shared behavior: expansion mode (single or multiple), keyboard navigation, loop wrapping, and animation timing. It does not render any visible UI itself — only a container element. HTML tag for the root element. Expansion mode. `"single"` allows only one item open at a time ( Disclosure pattern). `"multiple"` allows any number of items open simultaneously. Orientation of the accordion. Affects which arrow keys navigate between triggers and how focus moves. Whether keyboard navigation wraps from the last trigger back to the first, and vice versa. Disables all items in the accordion when `true`. Individual items can still override this with their own `disabled` prop. Duration of the expand/collapse animation in milliseconds, as a string. CSS class applied to the root element. #### Expansion Modes - **`single`** — Opening one item automatically closes the previously open item. Only one panel is visible at a time. This is the classic accordion behavior. - **`multiple`** — Each item toggles independently. Opening one does not affect others. Use this for disclosure-style UIs where users may want several sections visible. #### Keyboard Behavior When a trigger has focus: | Key | Action | |---|---| | `Enter` / `Space` | Toggle the associated panel. | | `ArrowDown` | Move focus to the next trigger (vertical orientation). | | `ArrowUp` | Move focus to the previous trigger (vertical orientation). | | `ArrowRight` | Move focus to the next trigger (horizontal orientation). | | `ArrowLeft` | Move focus to the previous trigger (horizontal orientation). | | `Home` | Move focus to the first trigger. | | `End` | Move focus to the last trigger. | When `loop` is `true`, pressing `ArrowDown` on the last trigger wraps to the first, and `ArrowUp` on the first wraps to the last. In RTL layouts, horizontal arrow keys are automatically reversed. #### Accessibility The root component generates unique IDs for each item and wires up ARIA attributes automatically: - Each trigger has `aria-expanded` and `aria-controls` pointing to its content panel. - Each content panel has `role="region"` and `aria-labelledby` pointing to its header. - `data-ome-state` attributes sync open/closed state for CSS targeting. --- ### Accordion Item Each `AccordionItem` represents a single expandable section. It is the direct child of the root `Accordion` and the parent of `AccordionHeader` and `AccordionContent`. The runtime discovers items by scanning for direct item children at initialization time. HTML tag for the item wrapper. Disables this specific item. Overrides the root `disabled` prop on a per-item basis. Initial open state. In `single` mode, only the last item with `open="true"` will actually render open. Additional CSS class added to the item when it is in the open state. Useful for CSS-driven animations or visibility changes. CSS class applied to the item element. --- ### Accordion Header The header provides semantic heading markup (`` – ``) for the trigger. It determines the heading level and can auto-generate a trigger when its slot is empty but a `content.label` value is provided. This is the simplest way to create an accordion — just set the label and move on. HTML tag for the header wrapper. Heading level for the header (`"1"` through `"6"`). Renders as `` – `` accordingly. Tag used for the auto-generated trigger's inner label wrapper when the header creates a trigger from `content.label`. Fallback trigger label. When the header slot is empty, this value is used to generate a trigger automatically. CSS class applied to the header element. #### Header Fallback Rendering When the header slot is empty and `content.label` has a value, the header auto-generates a trigger element. This means you can build a working accordion with just `AccordionHeader` and `AccordionContent` — no explicit `AccordionTrigger` needed: ``` Accordion Header (slot empty + label set) └── Auto-generated Trigger └── Label text └── Chevron icon ``` --- ### Accordion Trigger The interactive element users click, tap, or focus to toggle a panel. When used inside `AccordionHeader`, it receives heading semantics from the parent. When the slot is empty, the trigger renders a label wrapper and chevron icon using its `content.label` value. Tag for the fallback label wrapper only. **This does not change the outer button element** — the trigger always renders a ` ``` --- ### Accordion Content The content panel that expands and collapses. It is animated by default using the root's `animationDuration` setting. Content is hidden via `height: 0` and `overflow: hidden` when collapsed — it remains in the DOM at all times. HTML tag for the content panel. When `true`, the collapsed panel uses the browser's `hidden="until-found"` attribute instead of `height: 0`. This allows browser find-in-page (`Ctrl+F` / `Cmd+F`) to search inside collapsed panels and expand them automatically when a match is found. Use this when your accordion contains searchable reference content. CSS class applied to the content element. --- ## Common Mistakes Do not place another block between `Accordion` and `AccordionItem`. The runtime scans for **direct** item children at initialization. Adding a wrapper (like a Group or Stack block) between them breaks item discovery and the accordion will not initialize. `AccordionTrigger` `structure.tag` only changes the inner fallback label wrapper — the outer interactive element is always a ` Close {/slot} `} The trigger and dialog are siblings — the trigger sits wherever you need it in the page, and the dialog can be authored anywhere. They connect because both reference the same ID (`"newsletter"`). {` {#slot default} Product details Quick product overview. Product information goes here. {#slot default}×{/slot} {/slot} `} Use the `placement` setting to position the dialog anywhere in the viewport. When the trigger slot is empty, `content.label` renders a fallback text button. --- ## Family Components ### Dialog (Root) The root component holds all modal content and manages dialog behavior: focus trapping, scroll locking, close-on-escape, close-on-outside-click, overlay rendering, and viewport placement. It does not render visible UI itself until opened — only a hidden container. When opened, the runtime physically moves the dialog node into a shared host element at the end of `document.body`. Unique identifier for this dialog. `DialogTrigger` components use this ID to open the dialog via their `targetDialogId` prop. Must be unique across the page. Opens the dialog automatically when the runtime initializes. Useful for announcement or onboarding dialogs that should appear on page load. Whether pressing `Escape` closes the active dialog. Whether clicking the overlay backdrop closes the active dialog. Keeps keyboard focus trapped inside the open dialog. Tab and Shift+Tab cycle through focusable elements within the dialog only. Prevents background page scrolling while the dialog is open. Returns focus to the element that opened the dialog (typically the trigger) after the dialog closes. Controls where the dialog surface appears inside the viewport. `center` is the standard modal position. Other values align the dialog to the corresponding edge or corner. CSS selector targeting a specific element inside the dialog that should receive focus when it opens. When empty, focus moves to the first focusable element in the dialog. CSS class applied to the dialog surface element. #### Placement Options | Placement | Behavior | |---|---| | `center` | Centered both horizontally and vertically. Default modal position. | | `top` | Aligned to the top edge, centered horizontally. | | `bottom` | Aligned to the bottom edge, centered horizontally. | | `left` | Aligned to the left edge, centered vertically. | | `right` | Aligned to the right edge, centered vertically. | | `bottom-center` | Same as `bottom`. | | `bottom-left` | Bottom-left corner of the viewport. | | `bottom-right` | Bottom-right corner of the viewport. | #### CSS Custom Properties These properties are available on the dialog surface element (`.ome-dialog-default`) for runtime customization: | Property | Default | Description | |---|---|---| | `--ome-dialog-overlay-color` | `rgba(0,0,0,0.5)` | Background color of the overlay backdrop. | | `--ome-dialog-offset-x` | `0px` | Horizontal offset from the computed placement position. | | `--ome-dialog-offset-y` | `0px` | Vertical offset from the computed placement position. | #### Keyboard Behavior | Key | Action | |---|---| | `Escape` | Closes the dialog (when `closeOnEscape` is `true`). | | `Tab` | Moves focus to the next focusable element inside the dialog (when `trapFocus` is `true`). | | `Shift+Tab` | Moves focus to the previous focusable element inside the dialog (when `trapFocus` is `true`). | #### Accessibility The root component wires up ARIA attributes automatically: - The dialog surface gets `role="dialog"` and `aria-modal="true"`. - `aria-labelledby` is auto-wired to the `DialogTitle` element's generated ID. - `aria-describedby` is auto-wired to the `DialogDescription` element's generated ID (if present). - IDs are auto-generated when not explicitly set. #### Behaviors - **One dialog at a time.** Only one dialog can be active. Opening a new dialog closes the previous one without animation. - **DOM teleport.** The dialog node is physically moved into a shared host element at the end of `document.body` while open, then restored to its authored DOM position on close. This ensures the dialog overlays all page content regardless of where it was authored. --- ### DialogTrigger The interactive element users click to open a dialog. DialogTrigger is **always placed separately from Dialog** — they are siblings in the block tree, not parent and child. They connect via a shared ID: `targeting.targetDialogId` must match the dialog's `identity.dialogId`. Multiple triggers can target the same dialog. When the trigger slot is empty, `content.label` renders a fallback text button. ID of the dialog this trigger should open. Must match the `identity.dialogId` of a `Dialog` component on the page. Fallback trigger label used when the trigger slot is empty. Renders as a plain text button. Disables the trigger so it cannot open the dialog. CSS class applied to the trigger button element. #### Accessibility - Triggers get `aria-haspopup="dialog"`, `aria-expanded`, and `aria-controls` wired automatically. - Pressing `Enter` or `Space` on a focused trigger opens the target dialog. --- ### DialogTitle Provides accessible naming for the dialog. The root `Dialog` auto-wires `aria-labelledby` to this element's ID, so screen readers announce the title when the dialog opens. Must be placed inside the `Dialog` slot. HTML tag used for the title element. Common values: `"h2"`, `"h3"`, `"h4"`. CSS class applied to the title element. --- ### DialogDescription Provides additional context for screen readers. The root `Dialog` auto-wires `aria-describedby` to this element's ID. Must be placed inside the `Dialog` slot. HTML tag used for the description element. CSS class applied to the description element. --- ### DialogClose Button that closes the active dialog. Must be placed inside the `Dialog` slot. You can include any content in its slot — text, icons, or custom markup. Disables the close button so users cannot click it to dismiss the dialog. CSS class applied to the close button element. --- ## Common Mistakes `DialogTrigger` and `Dialog` are **siblings**, not parent and child. The trigger must be placed outside the dialog in the block tree. They connect by matching `targetDialogId` to `dialogId` — nesting breaks the connection and the trigger will not work. Without matching IDs, the trigger has no way to know which dialog to open. Both `identity.dialogId` on `Dialog` and `targeting.targetDialogId` on `DialogTrigger` must be set to the same value. Each dialog must have a unique `dialogId`. If two dialogs share the same ID, only the first one registers — the second will never open. When a dialog opens, its DOM node is physically moved into a shared runtime host at the end of `document.body`. It does not render in-place where you authored it. This ensures the overlay appears above all page content. --- ## FAQs
Can I have multiple triggers for one dialog? Yes. Any number of `DialogTrigger` components can target the same `dialogId`. Each trigger independently opens the same dialog. This is useful for pages that offer several entry points to the same form or overlay.
Can I have multiple dialogs on one page? Yes. Give each dialog a unique `dialogId` and have its triggers target that specific ID. Only one dialog can be active at a time — opening a new dialog closes the previous one without animation.
Does DialogTitle have to be inside Dialog? Yes. `DialogTitle` must be placed inside the `Dialog` slot. The runtime looks for it there to wire up `aria-labelledby`. If placed outside, the dialog will not have an accessible name.
What happens to the DOM when a dialog opens? The dialog's DOM node is physically moved from its authored position into a shared host element appended to `document.body`. When the dialog closes, the node is moved back to its original position. This teleport ensures the overlay always renders above all other page content.
How do I position the dialog in the viewport? Use the `settings.placement` prop on `Dialog`. Options are `center` (default), `top`, `bottom`, `left`, `right`, `bottom-center`, `bottom-left`, and `bottom-right`. For fine-tuning, use the CSS custom properties `--ome-dialog-offset-x` and `--me-dialog-offset-y` to shift the dialog from its computed position.
How do I change the overlay backdrop color? Set the `--ome-dialog-overlay-color` CSS custom property on the dialog surface element (`.ome-dialog-default`). The default is `rgba(0,0,0,0.5)`. For a lighter backdrop: `--ome-dialog-overlay-color: rgba(0,0,0,0.2)`. For a solid backdrop: `--ome-dialog-overlay-color: rgba(0,0,0,0.85)`.
Can I auto-focus a specific element when the dialog opens? Yes. Set `settings.initialFocusSelector` to a CSS selector matching the element you want focused (e.g. `"#email-input"`). When empty, focus moves to the first focusable element in the dialog.
--- ## Drawer ## Overview Use the Drawer family when you need a panel that slides in from the edge of the viewport — for mobile navigation, filter panels, quick settings, or any overlay content that should feel spatially connected to the screen edge. The family is composed of five components: a root panel, a remote trigger, a title, a description, and a close button. The trigger lives separately from the drawer — they connect by matching IDs, not by DOM nesting. ## Authoring Structure **Important:** `DrawerTrigger` is always placed separately from `Drawer` — they are siblings, not nested. They connect via matching IDs: `identity.drawerId` on the Drawer must match `targeting.targetDrawerId` on the Trigger. ``` DrawerTrigger (separate, anywhere on page) Drawer (the sliding panel) ├── DrawerTitle ├── DrawerDescription (optional) ├── (your content) └── DrawerClose ``` ### Placement Rules | Component | Placement | Role | |---|---|---| | **DrawerTrigger** | Separate from `Drawer`. Anywhere on the page. | Button that opens the drawer by targeting its ID. | | **Drawer** | Top-level. Can be anywhere in the DOM. | The sliding panel surface. Hosts title, description, content, and close. | | **DrawerTitle** | Inside `Drawer`. | Provides accessible name via `aria-labelledby`. Renders as a heading. | | **DrawerDescription** | Inside `Drawer`. Optional. | Provides accessible description via `aria-describedby`. | | **DrawerClose** | Inside `Drawer`. | Button that dismisses the drawer. | ## Quick Start {` {#slot default} Navigation Close {/slot} `} The trigger and drawer are siblings connected by matching IDs (`mobile-menu`). The trigger can be placed anywhere on the page — it does not need to be near the drawer. {` {#slot default}Filters{/slot} {#slot default} Filters Refine your search results. {#slot default}×{/slot} {/slot} `} Use custom slot content when you need icons, badges, or any markup beyond a plain label in the trigger or close button. --- ## Family Components ### Drawer (Root) The root component renders the sliding panel surface. It is targeted by one or more `DrawerTrigger` components via a shared ID. When opened, the drawer's DOM node is physically moved into a shared runtime host — it does not render at its authored position. On close, the node is restored to its original location using a Comment anchor. Unique ID that triggers use to target this drawer. Must match `targetDrawerId` on any trigger that should open it. Edge the drawer slides from. `"bottom"` creates a bottom sheet with a drag handle. Other directions slide in without a handle. Opens the drawer automatically when the runtime initializes. When `true`, clicking the overlay or pressing `Escape` closes the drawer. When `false`, the drawer can only be closed via `DrawerClose`. CSS class applied to the drawer surface element. #### Direction Behavior | Direction | Behavior | |---|---| | **`bottom`** | Slides up from the bottom edge. A drag handle element is auto-generated for bottom-sheet interaction. | | **`right`** | Slides in from the right edge. No handle element. | | **`top`** | Slides down from the top edge. No handle element. | | **`left`** | Slides in from the left edge. No handle element. | #### CSS Custom Properties | Property | Default | Description | |---|---|---| | `--ome-drawer-overlay-color` | `rgba(0, 0, 0, 0.5)` | Background color of the overlay behind the drawer. | | `--ome-drawer-animation-duration` | `250ms` | Duration of the slide in/out transition. | #### Key Behaviors - **One active drawer at a time** — opening a drawer closes any currently open drawer. - **DOM portaling** — the drawer node is moved into a shared runtime host while open and restored on close. - **Focus trap** — Tab/Shift+Tab is trapped inside the drawer while open. - **Focus restoration** — focus returns to the opener element when the drawer closes. - **Scroll lock** — body scrolling is disabled while the drawer is open, with `scrollbar-gutter: stable` to prevent layout shift. #### Accessibility - Drawer receives `role="dialog"` and `aria-modal="true"`. - `aria-labelledby` is wired to the `DrawerTitle` element's ID. - `aria-describedby` is wired to the `DrawerDescription` element's ID (if present). - The drag handle (bottom direction only) gets `aria-hidden="true"`. - IDs are auto-generated if not explicitly set. --- ### DrawerTrigger The trigger is a ` ``` When the trigger slot has content, the `content.triggerLabel` is used only as the `aria-label` for accessibility. #### Invalid Item Handling If an item is missing its trigger or full container, the runtime marks it as invalid: - `data-ome-invalid="true"` is set on the item element. - The trigger button is disabled (`disabled` + `aria-disabled="true"`). - The trigger cursor changes to `not-allowed` via the fixed styling. - The item is skipped during gallery initialization. --- ## CSS Custom Properties | Property | Default | Used By | Description | |---|---|---|---| | `--ome-lightbox-trigger-gap` | `var(--space-xs, 0.5rem)` | Trigger button children | Controls the gap between inline-flex children inside the trigger button. | | `--ome-lightbox-html-padding` | `24px` | HTML-mode slides | Viewport padding around HTML-mode slide content. Set from `presentation.htmlViewportPadding`. | ### PhotoSwipe CSS Custom Properties The following PhotoSwipe variables are set by the runtime and can be overridden in your own CSS: | Property | Default | Description | |---|---|---| | `--pswp-bg` | `#000` | Overlay background color. | | `--pswp-placeholder-bg` | `#222` | Placeholder background while images load. | | `--pswp-root-z-index` | `2147483647` | Z-index for the PhotoSwipe root element. | | `--pswp-icon-color` | `#fff` | Icon (close, arrows, zoom) fill color. | | `--pswp-icon-color-secondary` | `#4f4f4f` | Secondary icon color. | | `--pswp-icon-stroke-color` | `#4f4f4f` | Icon stroke color. | | `--pswp-icon-stroke-width` | `2px` | Icon stroke width. | | `--pswp-error-text-color` | `var(--pswp-icon-color)` | Error message text color. | | `--pswp-transition-duration` | _(PhotoSwipe default)_ | Transition duration for UI element fade. | --- ## Common Mistakes The full slot is the content the overlay displays. If it is empty, the item will be marked as invalid and skipped during gallery initialization. Always add content to the full slot. When using image mode, the full-slot `` must have explicit `width` and `height` attributes. Without deterministic dimensions, the runtime falls back to HTML mode, which loses PhotoSwipe's native pinch-to-zoom and responsive sizing. A console warning is logged when this fallback occurs. The trigger and full slots are separate by design. The trigger is what users click to open the lightbox. The full slot is what appears inside the overlay. They are not automatically shared or duplicated. Do not place another block between `Lightbox` and `Lightbox Item`. The runtime scans for direct `[data-ome-lightbox-item]` descendants of the root to build the gallery. Extra wrappers between the root and items will still work (the query uses `querySelectorAll`), but the positional index used for gallery navigation is based on DOM order — keep items as close to the root as possible for predictable behavior. The Lightbox family is slot-driven. There is no separate prop for specifying the full-size image URL — place the image in the full slot and the runtime extracts `src`, `srcset`, dimensions, and alt text automatically. --- ## FAQs
How does image mode detection work? The runtime checks if both the trigger slot and the full slot each contain exactly **one** `` element. If they do, and the full-slot image has explicit `width` and `height` attributes, it enters image mode. The full-slot image's `src` (or `data-src` / `data-lazy-src` for lazy-loaded images) becomes the lightbox source, and the trigger image's `src` becomes the placeholder thumbnail during the open animation. If any of these conditions are not met, the item falls back to HTML mode.
Can I mix image items and HTML items in the same gallery? Yes. Each item is independently detected as image mode or HTML mode. You can have image items alongside HTML items in the same Lightbox root. Arrow key navigation works across all items regardless of mode.
How do I customize the overlay appearance? Use the `presentation.bgOpacity` prop to change the overlay darkness, and override the PhotoSwipe CSS custom properties (like `--pswp-bg`, `--pswp-icon-color`) in your own stylesheet to change colors, z-index, and other visual aspects. The `presentation.htmlViewportPadding` prop controls the padding around HTML-mode slides.
Does Lightbox support lazy-loaded images? Yes. The runtime resolves lazy-loaded images by checking `data-src`, `data-lazy-src`, `data-srcset`, and `data-lazy-srcset` attributes as fallbacks. This is compatible with common lazy-loading plugins like WP Rocket and BJ Lazy Load. `` element sources are also resolved automatically.
Can I open the lightbox programmatically? The lightbox instance is stored on the root element as `element._omeLightbox`. You can use `element._omeLightbox.lightbox.loadAndOpen(index, dataSource)` to open the gallery at a specific item index. The runtime also syncs `data-ome-state` (`"open"` / `"closed"`) and `data-ome-current-index` on the root element for CSS targeting.
What happens if I have only one item? A single-item gallery works normally — the overlay opens with the item, and navigation arrows are automatically hidden by PhotoSwipe when there is only one slide.
--- ## Navigation Menu ## Overview Use the Navigation Menu family when you need authored navigation items with optional dropdown panels, keyboard navigation, animated viewport transitions, and a mobile drawer helper that automatically derives its structure from the desktop menu. The desktop family is composed of six components that nest together, plus a separate mobile helper and an explicit mobile-only node for complex mega-menu content. The root component manages open state, keyboard navigation, and optional animated viewport behavior. Navigation Menu Mobile is a **separate** component placed elsewhere on the page — it connects to the desktop menu by a shared `menuId` and builds a drawer-based mobile experience at runtime. ## Authoring Structure The desktop menu follows a strict nesting hierarchy: ``` Navigation Menu (desktop root) └── Navigation Menu List ├── Navigation Menu Item │ ├── plain link │ ├── OR Navigation Menu Trigger + Navigation Menu Content │ │ └── optional Navigation Menu Mobile Item subtree inside content │ └── OR link + Navigation Menu Trigger + Navigation Menu Content └── Navigation Menu Item ... Navigation Menu Mobile (separate — anywhere on the page) ``` ### Desktop and Mobile Are Separate Navigation Menu Mobile is **not** nested inside Navigation Menu. It is placed as a sibling element elsewhere on the page (typically inside the header shell alongside the desktop navigation). Both components share the same `menuId` value so the mobile helper can find and mirror the desktop structure at runtime. In header patterns, the typical structure is: ``` Header Shell ├── Brand ├── Navigation Menu (desktop, menuId="primary-nav") │ └── Navigation Menu List ... ├── Actions └── Navigation Menu Mobile (separate, menuId="primary-nav") ``` ### Placement Rules | Component | Placement | Role | |---|---|---| | **Navigation Menu** | Top-level wrapper for the desktop navigation. | Owns orientation, open-state coordination, viewport behavior, and the menu identity. | | **Navigation Menu List** | Direct child of `Navigation Menu`. | Groups authored top-level menu items. The runtime scans for this list when initializing. | | **Navigation Menu Item** | Direct child of `Navigation Menu List`. | One navigation entry — a plain link, a dropdown parent, or both. | | **Navigation Menu Trigger** | Inside `Navigation Menu Item`. | Interactive button that opens a dropdown panel. | | **Navigation Menu Content** | Inside `Navigation Menu Item`, alongside a trigger. | Dropdown or mega-menu panel content. | | **Navigation Menu Mobile Item** | Inside `Navigation Menu Content`. | Declares an explicit mobile tree for complex mega-menu content. | | **Navigation Menu Mobile** | **Separate** from the desktop menu — anywhere on the page, connected by `menuId`. | Mirrors the desktop menu into a drawer-based mobile experience at runtime. | ## Quick Start {` {#slot default} {#slot default} {#slot default} {#slot default}Products{/slot} {#slot default} Shop all products {/slot} {/slot} {#slot default} About {/slot} {/slot} {/slot} `} {` {#slot default} {#slot default} {#slot default} {#slot default}Services{/slot} {#slot default} Design Development {/slot} {/slot} {/slot} {/slot} `} The mobile helper reads the desktop menu structure at runtime and generates a drawer-based mobile navigation. No duplicate authoring is needed. --- ## Family Components ### Navigation Menu (Root) The root component wraps all desktop navigation items and manages shared behavior: orientation, keyboard navigation, animated viewport mode, and the menu identity used by the mobile helper. It does not render significant visible UI itself — only a container element and an optional viewport. HTML tag for the root element. Defaults to `nav` for proper landmark semantics. Shared menu ID that `Navigation Menu Mobile` uses to find and connect to this desktop menu. Set this to the same value on both components to enable mobile mirroring. Root orientation. Affects which arrow keys navigate between triggers and the layout direction. Use `vertical` for stacked or sidebar menu structures. Enables the generated viewport that animates dropdown panel changes. When enabled, content panels are portaled into a viewport container with smooth height transitions and directional motion. Makes the animated viewport align to a wider container instead of the trigger width. Requires `useAnimatedMenu` to be enabled and a `fullWidthTargetSelector` to be set. CSS selector used to find the container element whose width the viewport should match. Only visible when `useFullWidth` is enabled. Can be a class selector like `.site-shell`. CSS class applied to the root element. CSS class applied to the generated animated viewport container. Only visible when `useAnimatedMenu` is enabled. #### Animated Viewport Mode When `useAnimatedMenu` is `true`, the root component generates a viewport container that houses all dropdown panels. This enables: - Smooth height transitions as panels open and close. - Directional motion animations when switching between panels (content slides in from the direction of the newly focused trigger). - Full-width mode that aligns the viewport to a wider parent container for mega-menu layouts. Animated mode only activates for **horizontal** orientation and is automatically disabled inside the Etch builder (where it falls back to a non-animated preview). #### Keyboard Behavior When a trigger has focus in a **horizontal** menu: | Key | Action | |---|---| | `Enter` / `Space` | Toggle the associated panel. | | `ArrowRight` | Move focus to the next trigger. | | `ArrowLeft` | Move focus to the previous trigger. | | `ArrowDown` | Open the dropdown panel. | | `Home` | Move focus to the first trigger. | | `End` | Move focus to the last trigger. | | `Escape` | Close the open panel and return focus to its trigger. | In a **vertical** menu, `ArrowDown` / `ArrowUp` navigate between triggers and `ArrowRight` opens the panel. Navigation wraps by default — pressing `ArrowRight` on the last trigger focuses the first, and vice versa. #### Accessibility The runtime wires up ARIA attributes automatically: - Each trigger has `aria-expanded` and `aria-haspopup="menu"` that update as panels open and close. - Each content panel has `role="menu"` and syncs `aria-hidden` with its open state. - `data-ome-state` attributes (`open` / `closed`) are set on items, triggers, and content for CSS targeting. --- ### Navigation Menu List The list container groups top-level menu items. The runtime scans for the direct `Navigation Menu List` child when initializing the root component. CSS class applied to the list element. The list always renders as a `` element with `display: flex` and reset list styles. In horizontal mode the flex direction is `row`; in vertical mode it becomes `column`. --- ### Navigation Menu Item Each `NavigationMenuItem` represents one navigation entry. It can contain a plain link, a trigger-plus-content dropdown pair, or a combination of both (a link that also opens a dropdown). Disables this item and any trigger it contains. Disabled items cannot be opened or focused for activation. CSS class applied to the item element. The item always renders as an `` element. The runtime tracks open/closed state via `data-ome-state` on the item. --- ### Navigation Menu Trigger The interactive button users click or hover to open a dropdown panel. The trigger always renders as a `