Skip to content

Web components

Astropress ships a set of vanilla custom elements for the admin UI. They are importable individually, tree-shakeable, usable outside the Astropress package, and extendable via class inheritance.

Import paths

// Register all built-in elements (side-effect import)
import "astropress/web-components";
// Or import individual elements (tree-shakeable)
import "astropress/web-components/theme-toggle";
import "astropress/web-components/confirm-dialog";
import "astropress/web-components/html-editor";
import "astropress/web-components/admin-nav";
import "astropress/web-components/ap-stale-tab-warning";
import "astropress/web-components/notice";

Each import registers the custom element globally via customElements.define(). Import them in a <script> tag inside your .astro component.

Built-in elements

<ap-theme-toggle>

Wraps a button to provide light/dark theme switching. Reads and writes localStorage key "theme". Syncs all instances on the page. Responds to html[data-theme] via existing CSS variables — no shadow DOM needed.

<ap-theme-toggle label-dark="Switch to dark mode" label-light="Switch to light mode">
<button type="button" class="theme-toggle-admin" aria-pressed="false">
<span class="theme-toggle-icon" aria-hidden="true"></span>
</button>
</ap-theme-toggle>
<script>
import "astropress/web-components/theme-toggle";
</script>

Attributes:

AttributeDescription
label-darkaria-label shown when theme is currently light (clicking will go dark)
label-lightaria-label shown when theme is currently dark (clicking will go light)

<ap-confirm-dialog>

A generic confirmation dialog that responds to trigger buttons anywhere on the page. Replaces both comments-dialog.ts and redirects-dialog.ts with one element.

<!-- The dialog (place near the bottom of your page) -->
<ap-confirm-dialog>
<dialog id="delete-dialog" class="confirm-modal" aria-labelledby="delete-dialog-title">
<div class="modal-content">
<h2 id="delete-dialog-title">Delete item?</h2>
<p>This action cannot be undone.</p>
<p><strong id="item-name"></strong></p>
<div class="modal-actions">
<button type="button" data-dialog-close>Cancel</button>
<form method="post" action="/ap-admin/actions/item-delete">
<input type="hidden" name="itemId" value="" />
<button type="submit">Delete</button>
</form>
</div>
</div>
</dialog>
</ap-confirm-dialog>
<!-- The trigger (can be anywhere on the page) -->
<button
type="button"
data-confirm-trigger
data-dialog-id="delete-dialog"
data-text-item-name="My Item"
data-field-name="itemId"
data-field-value="abc-123"
>
Delete
</button>
<script>
import "astropress/web-components/confirm-dialog";
</script>

Trigger attributes:

AttributeDescription
data-confirm-triggerMarks this button as a dialog trigger
data-dialog-idID of the <dialog> to open
data-text-[element-id]Sets textContent on #[element-id] inside the dialog
data-field-nameName of the <input> to populate inside the dialog’s form
data-field-valueValue to set on that input

Close triggers: Any element with data-dialog-close closes the dialog when clicked.


<ap-html-editor>

Wraps a textarea editor with a formatting toolbar, live preview iframe, media library dialog, and a URL input dialog.

<ap-html-editor>
<form method="post" action="/ap-admin/actions/content-save">
<div role="toolbar" aria-label="Format body">
<button type="button" data-cmd="bold" aria-label="Bold"><strong>B</strong></button>
<button type="button" data-cmd="italic" aria-label="Italic"><em>I</em></button>
<button type="button" data-cmd="insertUnorderedList" aria-label="Bullet list">List</button>
<button type="button" data-cmd="createLink" aria-label="Insert link">Link</button>
<button type="button" class="insert-media-btn" aria-label="Open media library">Media</button>
</div>
<textarea data-body-editor name="body" rows="14"></textarea>
<div class="preview-frame">
<iframe sandbox="" title="Rendered preview"></iframe>
</div>
</form>
<!-- URL input dialog — place outside the form to avoid nested-form issues -->
<dialog id="url-input-dialog" aria-labelledby="url-input-title">
<h2 id="url-input-title">Insert link</h2>
<form id="url-input-form" method="dialog">
<input id="url-input-field" type="url" name="url" placeholder="https://" />
<button type="button" data-dialog-close>Cancel</button>
<button type="submit">Insert link</button>
</form>
</dialog>
<!-- Media library dialog -->
<dialog id="media-library-dialog" aria-labelledby="media-dialog-title">
<h2 id="media-dialog-title">Media Library</h2>
<button id="media-dialog-close" type="button">Close</button>
</dialog>
</ap-html-editor>
<script>
import "astropress/web-components/html-editor";
</script>

<ap-admin-nav>

The sidebar navigation element. Highlights the current page using aria-current="page" and supports keyboard navigation between links.

<ap-admin-nav>
<nav aria-label="Main navigation">
<ul>
<li><a href="/ap-admin" aria-current="page">Dashboard</a></li>
<li><a href="/ap-admin/posts">Posts</a></li>
<li><a href="/ap-admin/media">Media</a></li>
</ul>
</nav>
</ap-admin-nav>
<script>
import "astropress/web-components/admin-nav";
</script>

The component derives aria-current from window.location.pathname — no attribute needed.


<ap-stale-tab-warning>

Shows an accessible warning banner when another browser tab is editing the same content, or when the page has been open longer than the session TTL.

Uses the BroadcastChannel API — cross-tab communication with no server round-trips.

<ap-stale-tab-warning
slug="my-post-slug"
session-ttl-ms="3600000"
>
</ap-stale-tab-warning>
<script>
import "astropress/web-components/ap-stale-tab-warning";
</script>

Attributes:

AttributeDefaultDescription
slugIdentifies the content being edited
session-ttl-ms3600000 (1 hr)Shows a reload warning after this many ms without a page refresh

When triggered, renders a role="alert" banner — screen readers announce it immediately.


<ap-notice>

A transient notification banner with an accessible live region.

<ap-notice dismiss-after="4000">Post saved.</ap-notice>
<script>
import "astropress/web-components/notice";
</script>

Attributes:

AttributeDefaultDescription
dismiss-afterRemoves the element after this many milliseconds

The element renders with role="status" and aria-live="polite". Create notices dynamically:

const notice = document.createElement("ap-notice");
notice.setAttribute("dismiss-after", "3000");
notice.textContent = "Saved!";
document.body.appendChild(notice);

Design principles

  • Light DOM — no attachShadow(). Admin CSS custom properties inherit directly without ::part() selectors.
  • Progressive enhancement — server renders semantic HTML; the WC adds the interactive layer on the client.
  • Attribute-driven state — server passes initial state via HTML attributes.
  • AbortController for cleanup — each WC stores an AbortController in connectedCallback and calls .abort() in disconnectedCallback.

Extending a built-in element

import { ApConfirmDialog } from "astropress/web-components/confirm-dialog";
class MyConfirmDialog extends ApConfirmDialog {
connectedCallback() {
super.connectedCallback();
// Add custom behavior here
}
}
customElements.define("my-confirm-dialog", MyConfirmDialog);

Writing a new element

export class ApMyElement extends HTMLElement {
private _abortController: AbortController | null = null;
connectedCallback() {
this._abortController = new AbortController();
const { signal } = this._abortController;
this.querySelector("button")?.addEventListener("click", () => {
// handle click
}, { signal });
}
disconnectedCallback() {
this._abortController?.abort();
this._abortController = null;
}
}
customElements.define("ap-my-element", ApMyElement);

Key rules: light DOM only, use this.querySelector(), pass { signal } to every addEventListener, export the class, use ap- prefix.


Screen reader usage

The admin panel is fully navigable with screen readers.

Landmarks: <header>, <nav aria-label="Main navigation">, <main>, <footer>. Use your screen reader’s landmark navigation shortcut.

Headings: Each page has a structured heading hierarchy (<h1> page title, <h2> sections). Use heading navigation to jump to sections.

Forms: All controls have explicit <label> elements or aria-label attributes. Error messages are associated via aria-describedby.

Dialogs: <ap-confirm-dialog> uses the native <dialog> element — focus moves in on open, Escape closes, focus returns to trigger.

Skip link: A visually-hidden skip link (Skip to main content) appears at the top of every admin page and becomes visible on keyboard focus.