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:
| Attribute | Description |
|---|---|
label-dark | aria-label shown when theme is currently light (clicking will go dark) |
label-light | aria-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:
| Attribute | Description |
|---|---|
data-confirm-trigger | Marks this button as a dialog trigger |
data-dialog-id | ID of the <dialog> to open |
data-text-[element-id] | Sets textContent on #[element-id] inside the dialog |
data-field-name | Name of the <input> to populate inside the dialog’s form |
data-field-value | Value 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:
| Attribute | Default | Description |
|---|---|---|
slug | — | Identifies the content being edited |
session-ttl-ms | 3600000 (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:
| Attribute | Default | Description |
|---|---|---|
dismiss-after | — | Removes 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
AbortControllerinconnectedCallbackand calls.abort()indisconnectedCallback.
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.