Skip to content

Components

The built-in pyjinhx.builtins components, rendered here at docs build time. Each shows a live render and its PascalCase HTML; expand the accordions for props, style tokens, the DOM contract, and the Python form. Import them from pyjinhx.builtins (see the API index). PJXLazyLoad needs a backend, so it has no live render.

Conventions: Markup classes use the pjx- prefix; overrides use --pjx- custom properties. Builtin CSS also references theme variables (--surface, --border, --text, --radius-md, --shadow-md, --transition, --brand, …)—define those in your global CSS or map them to your design system. See builtin-conventions.md for the full per-component contract (auto-id, class_name, extra_attrs, js/css, headless IIFE JS under window.pjx, cancelable pjx:*:before-* events). Every component inherits id and render() from BaseComponent; most builtins additionally declare their own js/css, class_name, and extra_attrs fields. id is omitted from the props tables below.

Children-vs-content tag gotcha (children-mapping components): Several components map children to a single attribute (e.g. PJXNotification content, PJXTooltip content). If you write these as PascalCase tags in a template, do not supply both child text and the corresponding attribute on the same tag—use body text as the child or the attribute, not both.

Backdrop click (PJXModal and PJXDrawer): Both render a native <dialog>; a document click listener treats a click whose target is the <dialog> root itself (the backdrop) as a close. Any native <dialog> clicked directly is affected—use unique ids and avoid stacking multiple dialogs unless you adjust this.

Static

PJXBadge

Small status label. Assets: pjx_badge.css only.

<PJXBadge label="Active" color="brand"/>
Props & API
Field Type Default Description
label str "" Inner text.
color literal "neutral" brand, error, neutral, mutedpjx-badge--{color}.
shape literal "md" square, sm, md, fullpjx-badge--{shape}.
removable bool False Renders a remove button (data-pjx-badge-remove).
remove_label str "Remove" aria-label for the remove button.
remove_attrs dict {} Extra attributes on the remove button (e.g. hx-post to wire removal).
Style tokens
Token Default (maps to)
--pjx-badge-font-size var(--font-size-xs)
--pjx-badge-radius-sm var(--radius-sm)
--pjx-badge-radius-md var(--radius-md)
--pjx-badge-radius-full var(--radius-full)
--pjx-badge-brand-bg var(--brand-subtle)
--pjx-badge-brand-fg var(--brand-muted)
--pjx-badge-brand-accent var(--brand)
--pjx-badge-error-bg var(--error-bg)
--pjx-badge-error-fg var(--error)
--pjx-badge-error-border var(--error-border)
--pjx-badge-neutral-bg var(--surface-alt)
--pjx-badge-neutral-fg var(--text)
--pjx-badge-neutral-border var(--border)
--pjx-badge-muted-bg var(--surface)
--pjx-badge-muted-fg var(--text-muted)
--pjx-badge-muted-border var(--border)
DOM & classes

Root .pjx-badge; no JS API.

Classes: pjx-badge; color modifiers pjx-badge--brand, --error, --neutral, --muted; shape pjx-badge--square, --sm, --md, --full; removable adds pjx-badge--removable and a .pjx-badge__remove button. Wire removal yourself (e.g. remove_attrs={"hx-post": "..."}); the button has no built-in behavior.

Python
PJXBadge(label="Active", color="brand")

PJXCard

<article> shell for composed card layouts. Compose with PJXCardHeader, PJXCardBody, and PJXCardFooter parts. Assets: pjx_card.css only.

<PJXCard>
  <PJXCardHeader title="Quarterly report"/>
  <PJXCardBody>Revenue grew 12% over Q1.</PJXCardBody>
  <PJXCardFooter>Updated today</PJXCardFooter>
</PJXCard>
Props & API
Field Type Default Description
class_name str "" Extra CSS class(es) on the root element.
content str \| BaseComponent "" Pre-rendered children (header + body + footer parts).
Style tokens
Token Default
--pjx-card-bg var(--surface-alt)
--pjx-card-border var(--border)
--pjx-card-radius var(--radius-md)
--pjx-card-title-color var(--text)
--pjx-card-padding var(--space-4, 1rem)
DOM & classes

Root article.pjx-card; no JS API.

Classes: pjx-card.

Python
PJXCard(
    content=PJXCardHeader(title="Quarterly report").render()
    + PJXCardBody(content="Revenue grew 12% over Q1.").render()
    + PJXCardFooter(content="Updated today").render()
)

PJXCardHeader

Card header region. When title is set it renders <h3 class="pjx-card__title"> inside the header; when title is empty the content slot is rendered directly instead. Assets: pjx_card_header.css only.

<PJXCard>
  <PJXCardHeader title="Quarterly report"/>
  <PJXCardBody></PJXCardBody>
  <PJXCardFooter></PJXCardFooter>
</PJXCard>
Props & API
Field Type Default Description
title str "" Convenience shorthand — renders <h3 class="pjx-card__title"> when set; takes precedence over content.
class_name str "" Extra CSS class(es) on the root element.
content str \| BaseComponent "" Rich header slot; used when title is empty.
DOM & classes

Root header.pjx-card__header; no JS API.

Classes: pjx-card__header, pjx-card__title.

Python
PJXCardHeader(title="Quarterly report")

PJXCardBody

Card body region. Assets: pjx_card_body.css only.

<PJXCard>
  <PJXCardHeader title="Quarterly report"/>
  <PJXCardBody>Revenue grew 12% over Q1.</PJXCardBody>
  <PJXCardFooter></PJXCardFooter>
</PJXCard>
Props & API
Field Type Default Description
class_name str "" Extra CSS class(es) on the root element.
content str \| BaseComponent "" Main card content.
DOM & classes

Root div.pjx-card__body; no JS API.

Classes: pjx-card__body.

Python
PJXCardBody(content="Revenue grew 12% over Q1.")

PJXCardFooter

Card footer region. Assets: pjx_card_footer.css only.

<PJXCard>
  <PJXCardHeader title="Quarterly report"/>
  <PJXCardBody></PJXCardBody>
  <PJXCardFooter>Updated today</PJXCardFooter>
</PJXCard>
Props & API
Field Type Default Description
class_name str "" Extra CSS class(es) on the root element.
content str \| BaseComponent "" Footer content (actions, metadata, etc.).
DOM & classes

Root footer.pjx-card__footer; no JS API.

Classes: pjx-card__footer.

Python
PJXCardFooter(content="Updated today")

PJXCarousel

Compound image/content carousel with arrows, dots, keyboard nav, swipe, and opt-in autoplay. Compose with PJXCarouselSlide. Assets: pjx_carousel.css, pjx_carousel.js.

<PJXCarousel label="Product photos">
  <PJXCarouselSlide><img src="/photo-1.jpg" alt="Front view"></PJXCarouselSlide>
  <PJXCarouselSlide><img src="/photo-2.jpg" alt="Side view"></PJXCarouselSlide>
  <PJXCarouselSlide><img src="/photo-3.jpg" alt="Back view"></PJXCarouselSlide>
</PJXCarousel>
Props & API
Field Type Default Description
label str "Carousel" aria-label on the root region.
loop bool True Wrap at the ends. When False, the prev/next arrows disable (aria-disabled) at the first/last slide.
autoplay bool False Auto-advance on a timer. Always ships a play/pause toggle button when enabled, and pauses on hover/focus/pointer interaction — never a silent, unpausable auto-advance.
interval_ms int 5000 Autoplay interval in milliseconds. Ignored when autoplay=False.
prev_label str "Previous slide" aria-label on the previous-slide arrow button.
next_label str "Next slide" aria-label on the next-slide arrow button.
autoplay_toggle_label str "Pause autoplay" aria-label on the autoplay pause/play toggle button (only rendered when autoplay=True).
class_name str "" Extra CSS class(es) appended to the root element.
content str \| BaseComponent "" Composed interior — one or more PJXCarouselSlides.

Respects prefers-reduced-motion: reduce: disables the slide transition and disables autoplay outright.

Style tokens
Token Default
--pjx-carousel-border var(--border)
--pjx-carousel-bg var(--surface)
--pjx-carousel-radius var(--radius-md)
--pjx-carousel-arrow-bg color-mix(in srgb, var(--surface) 70%, transparent)
--pjx-carousel-arrow-fg var(--text)
--pjx-carousel-dot-bg var(--border)
--pjx-carousel-dot-active-bg var(--brand)
DOM & classes

Root div.pjx-carousel[role="region"][data-pjx-carousel]. pjx_carousel.js discovers [data-pjx-carousel-slide] descendants at init, builds dots, and delegates click/keyboard/pointer events on [data-pjx-carousel]. Fires a cancelable pjx:carousel:before-change then pjx:carousel:change on the root, detail = {index, reason}.

Classes: pjx-carousel, pjx-carousel__track, pjx-carousel__arrow, pjx-carousel__dots, pjx-carousel__dot, pjx-carousel__autoplay-toggle.

Python
PJXCarousel(
    label="Product photos",
    content=(
        PJXCarouselSlide(content='<img src="/photo-1.jpg" alt="Front view">').render()
        + PJXCarouselSlide(content='<img src="/photo-2.jpg" alt="Side view">').render()
        + PJXCarouselSlide(content='<img src="/photo-3.jpg" alt="Back view">').render()
    ),
)

PJXCarouselSlide

Single carousel slide (any content, not just images). Assets: pjx_carousel_slide.css.

<PJXCarouselSlide label="Front view">
  <img src="/photo-1.jpg" alt="Front view">
</PJXCarouselSlide>
Props & API
Field Type Default Description
label str "" Optional per-slide aria-label prefix (e.g. "Front view"). The engine appends "— N of M" at init regardless of whether label is set.
class_name str "" Extra CSS class(es) appended to the root element.
content str \| BaseComponent "" Slide content — typically an <img>, but any markup is valid.
DOM & classes

Root div.pjx-carousel__slide[role="group"][data-pjx-carousel-slide].

Classes: pjx-carousel__slide.

Python
PJXCarouselSlide(
    label="Front view", content='<img src="/photo-1.jpg" alt="Front view">'
)

PJXDivider

Separator line. Assets: pjx_divider.css only.

<PJXDivider orientation="horizontal" label="or continue with"/>
Props & API
Field Type Default Description
orientation literal "horizontal" horizontal (default hr) or vertical (bar).
label str "" If set with horizontal orientation, flex row with label between lines.
Style tokens
Token Default
--pjx-divider-color var(--border)
--pjx-divider-thickness 1px
--pjx-divider-gap 0.75rem
--pjx-divider-label-color var(--text-muted)
--pjx-divider-label-size var(--font-size-sm)
DOM & classes

Root .pjx-divider; no JS API.

Classes: pjx-divider--horizontal, pjx-divider--vertical, pjx-divider--labeled, pjx-divider__line, pjx-divider__label.

Python
PJXDivider(orientation="horizontal", label="or continue with")

PJXSpinner

Inline loading indicator. Assets: pjx_spinner.css only.

<PJXSpinner size="sm" label="Loading data"/>
Props & API
Field Type Default Description
size literal "md" sm, md, or lg.
label str "Loading" Visually hidden; exposed to assistive tech.
Style tokens
Token Default
--pjx-spinner-sm 1.25rem
--pjx-spinner-md 2rem
--pjx-spinner-lg 2.75rem
--pjx-spinner-track color-mix(in srgb, var(--text-muted) 35%, transparent)
--pjx-spinner-accent var(--brand)
DOM & classes

Root .pjx-spinner; no JS API.

Classes: pjx-spinner, pjx-spinner--sm|md|lg, pjx-spinner__ring, pjx-spinner__label (screen-reader-only).

Python
PJXSpinner(size="sm", label="Loading data")

PJXAvatar

Image or initials in a circle. Assets: pjx_avatar.css only.

<PJXAvatar initials="JD" size="sm" alt="Jane Doe"/>
Props & API
Field Type Default Description
src str "" Image URL; when empty, initials fallback is shown.
alt str "" img alt text; also used as title on initials.
initials str "" Up to two characters (trimmed/capped in validation).
size str \| int "md" Named token (sm, md, lg) or an arbitrary pixel size (int) or CSS length string ("36px", "2.5rem"). Named tokens emit the BEM modifier class; an int/CSS length renders width/height inline.
shape Literal["circle", "square", "hexagon", "diamond", "triangle"] "circle" "circle" is the implicit default and emits no modifier class; the others emit pjx-avatar--{shape}.
color str "" Inline background color (e.g. "#4f46e5", "hsl(240 60% 50%)" ).

Arbitrary pixel sizing: pass an int for a pixel size or any CSS length string for a custom size. This bypasses the named-token classes so the avatar can be sized by data rather than the three design-system tokens.

Non-circle shapes: hexagon, diamond, and triangle are cut with clip-path, which clips the border along with the shape — expect a straight-edged border on those, not a rounded one.

<PJXAvatar initials="JD" size="lg" shape="hexagon" alt="Hexagon"/>
Style tokens
Token Default
--pjx-avatar-sm 2rem
--pjx-avatar-md 2.5rem
--pjx-avatar-lg 3.25rem
--pjx-avatar-bg var(--surface-alt)
--pjx-avatar-fg var(--text-muted)
--pjx-avatar-border var(--border)
DOM & classes

Root .pjx-avatar; no JS API.

Classes: pjx-avatar, pjx-avatar--sm|md|lg (named tokens only), pjx-avatar--square|hexagon|diamond|triangle (non-circle shapes only), pjx-avatar__img, pjx-avatar__initials.

Python
PJXAvatar(initials="JD", size="sm", alt="Jane Doe")
PJXAvatar(initials="JD", size="lg", shape="hexagon", alt="Jane Doe")

PJXAvatarStack

Overlapping row of avatars with optional overflow count. Assets: pjx_avatar_stack.css only.

<PJXAvatarStack extra_count="4">
  <PJXAvatar initials="AB" size="sm" alt="Alice Brown"/>
  <PJXAvatar initials="CD" size="sm" alt="Carol Davis"/>
  <PJXAvatar initials="EF" size="sm" alt="Eve Foster"/>
</PJXAvatarStack>
Props & API
Field Type Default Description
avatars list[dict \| str \| BaseComponent] [] Per-avatar items — see below for the two accepted shapes.
extra_count int 0 When > 0, renders a +N overflow chip.
empty_label str "" When no avatars and empty_label is set, renders a fallback label.

Two item shapes are accepted in avatars:

  1. Structured data dict — the stack renders its own pill; no child PJXAvatar needed.
Key Type Required Description
initials str yes Up to two characters shown in the pill.
color str no Inline background color.
alt str no title/aria-label (takes precedence over name).
name str no Fallback title/aria-label when alt is not set.
  1. Pre-built item — an HTML string or any BaseComponent instance (original interface, still fully supported).
Style tokens
Token Default
--pjx-avatar-stack-overlap -0.5rem
--pjx-avatar-stack-ring var(--surface)
DOM & classes

Root .pjx-avatar-stack; no JS API. A structured-dict pill's alt/name sets aria-label, not just title, so screen readers get a name too; the +N overflow chip carries aria-label="N more".

Classes: pjx-avatar-stack, pjx-avatar-stack__pill (dict-rendered pills), pjx-avatar-stack__more, pjx-avatar-stack__empty.

Python
PJXAvatarStack(
    avatars=[
        PJXAvatar(initials="AB", size="sm", alt="Alice Brown"),
        PJXAvatar(initials="CD", size="sm", alt="Carol Davis"),
        PJXAvatar(initials="EF", size="sm", alt="Eve Foster"),
    ],
    extra_count=4,
)

PJXBreadcrumb

Ordered trail of links. Assets: pjx_breadcrumb.css only.

<PJXBreadcrumb items='[["Home","/"],["Projects","/projects"],["Dashboard",null]]'/>
Props & API
Field Type Default Description
items list[tuple[str, str \| None]] [] (label, href) left to right; href None marks the current page.
aria_label str "PJXBreadcrumb" aria-label on the <nav> wrapper.

items may also be passed as a JSON array string (e.g. from PascalCase tags): [["Home","/"],["Here",null]].

Style tokens
Token Default
--pjx-breadcrumb-sep "/"
--pjx-breadcrumb-link-color var(--brand)
--pjx-breadcrumb-current-color var(--text)
DOM & classes

Root .pjx-breadcrumb; no JS API.

Classes: pjx-breadcrumb, pjx-breadcrumb__list, pjx-breadcrumb__item, pjx-breadcrumb__link, pjx-breadcrumb__current. Separators via ::after on items except the last.

Python
PJXBreadcrumb(items=[("Home", "/"), ("Projects", "/projects"), ("Dashboard", None)])

PJXSkeleton

Placeholder shimmer blocks. Assets: pjx_skeleton.css only.

<PJXSkeleton variant="text" lines="3"/>
Props & API
Field Type Default Description
variant literal "text" text (stacked lines), circle, or rect.
lines int 3 For text, count of pjx-skeleton__line rows.
Style tokens
Token Default
--pjx-skeleton-bg var(--surface-alt)
--pjx-skeleton-shine color-mix(in srgb, var(--text) 8%, var(--surface-alt))
--pjx-skeleton-line-height 0.65rem
--pjx-skeleton-line-gap 0.5rem
--pjx-skeleton-circle-size 2.5rem
--pjx-skeleton-rect-height 6rem
--pjx-skeleton-rect-radius var(--radius-md)
--pjx-skeleton-duration 1.2s
DOM & classes

Root .pjx-skeleton; no JS API.

Classes: pjx-skeleton; variant modifiers pjx-skeleton--text, --circle, --rect; pjx-skeleton__line, pjx-skeleton__circle, pjx-skeleton__rect.

Python
PJXSkeleton(variant="text", lines=3)

PJXProgress

Determinate or indeterminate meter. Assets: pjx_progress.css only.

<PJXProgress value="65" max="100" label="Upload progress"/>
Props & API
Field Type Default Description
value float \| None None Omit or None for indeterminate <progress>.
max float 100 Passed to <progress max="…">.
label str "" Optional pjx-progress__label; wires aria-labelledby when set.
loading_label str "Loading" aria-label fallback on <progress> when label is empty.
Style tokens
Token Default
--pjx-progress-height 0.5rem
--pjx-progress-radius var(--radius-full)
--pjx-progress-track var(--surface-alt)
--pjx-progress-fill var(--brand)
--pjx-progress-indeterminate-speed 1.2s
DOM & classes

Root .pjx-progress; no JS API.

Classes: pjx-progress, pjx-progress__label, pjx-progress__bar.

Python
PJXProgress(value=65, max=100, label="Upload progress")

PJXEmptyState

Centered, vertically-stacked empty-view container. Compose whatever you like inside via content. Assets: pjx_empty_state.css only (template file pjx_empty_state.pjx next to pjx_empty_state.py).

<PJXEmptyState>
  <h3>No results</h3>
  <p>Try a different search term.</p>
</PJXEmptyState>
Props & API
Field Type Default Description
content str \| BaseComponent "" Freeform inner content (rendered as-is).
suggestions list[dict] [] Interactive suggestion chips; each dict dispatches a custom event on click. See below.
class_name str "" Extra CSS class(es) appended to the root element.

Suggestion chips (suggestions) provide a first-class "click a chip → dispatch an event" pattern — the common use case of filling an input or triggering a quick action without navigation.

Each item in suggestions is a dict with:

Key Type Required Description
label str yes Visible chip text.
value str no Value dispatched in the event payload; defaults to label.
event str no Custom event name; defaults to "pjx:suggestion".

The rendered chip uses Alpine's $dispatch, so any parent can listen with @pjx:suggestion.window="..." (or the custom event name). The dispatched detail is { value: "<chip value>" }.

<!-- Listener in a parent template -->
<textarea @pjx:suggestion.window="$el.value = $event.detail.value"></textarea>
Style tokens
Token Default
--pjx-empty-state-padding 2rem 1.5rem
--pjx-empty-state-max-width 28rem
--pjx-empty-state-gap 0.5rem
--pjx-empty-state-actions-gap 0.5rem (chip row gap)
--pjx-empty-state-chip-bg var(--surface-alt)
--pjx-empty-state-chip-color var(--text-muted)
--pjx-empty-state-chip-border var(--border)
DOM & classes

Root .pjx-empty-state; no JS API (suggestion chips require Alpine for $dispatch).

Classes: pjx-empty-state, pjx-empty-state__suggestions, pjx-empty-state__chip.

Python
PJXEmptyState(
    content="<h3>No results</h3><p>Try a different search term.</p>",
    suggestions=[{"label": "Draft a message"}, {"label": "Summarise a thread"}],
)

PJXIcon

Inline SVG icon from a vendored Lucide set (ISC). Assets: none — the SVG carries its own sizing attributes.

<PJXIcon name="plus" size="24" label="Add"/>
Props & API
Field Type Default Description
name str (required) Icon key into the vendored set (e.g. chevron-right, plus, search). An unknown name renders a hidden placeholder (nothing visible) and logs a warning.
size int \| str 16 intwidth/height in px; str → used verbatim as a CSS length.
stroke_width float 1.5 SVG stroke-width.
label str \| None None Nonearia-hidden="true"; a string → role="img" + a <title>.
DOM & classes

Root <svg> with stroke="currentColor" and fill="none"; no JS API. It inherits text color, so it themes for free — set color on the icon or any ancestor. Compose into content, e.g. <PJXButton variant="primary"><PJXIcon name="plus"/> Add</PJXButton>.

Classes: pjx-icon. No --pjx-icon-* tokens — size comes from the size/stroke_width props and color from currentColor.

Python
PJXIcon(name="plus", size=24, label="Add")

PJXButton

Structural, themeable button. Composes PJXRegionLoader for the inline loading state. Assets: pjx_button.css only.

<PJXButton content="Save" variant="primary"/>
Props & API
Field Type Default Description
content str \| BaseComponent "" Freeform button content (rendered raw); nest icons, text, or any markup here. The loading spinner auto-appends after this.
variant str "default" Class hook only → pjx-button--{variant}. No baked palette; style it yourself.
block bool False Full-width (pjx-button--block) instead of content-width.
loading bool False Renders a <PJXRegionLoader/> overlay and sets aria-busy="true".
disabled bool False Sets the disabled attribute.
type literal "button" button, submit, or reset.
Style tokens
Token Default
--pjx-button-bg transparent
--pjx-button-color inherit
--pjx-button-border 1px solid currentColor
--pjx-button-radius 0.375rem
--pjx-button-padding 0.5rem 0.875rem
--pjx-button-gap 0.5rem
--pjx-button-font-size inherit
--pjx-button-font-weight 500
--pjx-button-bg-hover var(--pjx-button-bg)
--pjx-button-border-hover currentColor
DOM & classes

Root <button> rendering {{ content }} verbatim. No JS API — pass hx-*/@click inline (they pass through to the root). The default .pjx-button is visually neutral so it never fights your design system; paint variants via .pjx-button--<variant>.

Classes: pjx-button; pjx-button--block; variant seams pjx-button--<variant>.

An icon-only button (content is just a PJXIcon, which renders aria-hidden) has no accessible name on its own — pass aria-label (it passes through to the root like any other attribute): <PJXButton aria-label="Delete"><PJXIcon name="trash"/></PJXButton>.

Python
PJXButton(content="Save", variant="primary")

PJXAccordion

Collapsible section built on native <details>. Composed with PJXAccordionTrigger (the <summary>, with the auto chevron) and PJXAccordionContent (the body). Assets: pjx_accordion.css (shell radius only — trigger and chevron CSS ship with PJXAccordionTrigger). No JS.

<PJXAccordion>
  <PJXAccordionTrigger>What is pyjinhx?</PJXAccordionTrigger>
  <PJXAccordionContent><p>A Python/Jinja HTML component framework.</p></PJXAccordionContent>
</PJXAccordion>
Props & API
Field Type Default Description
open bool True Initial expanded state (renders the open attribute on <details>).
group str \| None None Native <details name="..."> for exclusive-open groups; default is independent multi-open.
content str \| BaseComponent "" Inner HTML; nest a PJXAccordionTrigger + PJXAccordionContent here.
Style tokens
Token Default
--pjx-accordion-radius 0
--pjx-accordion-trigger-bg transparent
--pjx-accordion-trigger-color inherit
--pjx-accordion-trigger-padding 0.5rem 0.75rem
--pjx-accordion-trigger-font-size inherit
--pjx-accordion-trigger-font-weight 500
--pjx-accordion-trigger-bg-hover var(--pjx-accordion-trigger-bg)
--pjx-accordion-chevron-size 1em
DOM & classes

Root <details class="pjx-accordion"> rendering {{ content }} verbatim. Place a PJXAccordionTrigger and a PJXAccordionContent inside content to form a complete accordion.

Classes: pjx-accordion.

Python
PJXAccordion(
    content=PJXAccordionTrigger(content="What is pyjinhx?").render()
    + PJXAccordionContent(
        content="<p>A Python/Jinja HTML component framework.</p>"
    ).render()
)

PJXAccordionTrigger

The <summary> part of a composed accordion. Composes PJXIcon for the disclosure chevron. Assets: pjx_accordion_trigger.css (trigger + chevron styles), pjx_accordion_trigger.js (toggle-suppression for opt-in pjx-accordion__actions).

<PJXAccordion>
  <PJXAccordionTrigger>What is pyjinhx?</PJXAccordionTrigger>
  <PJXAccordionContent></PJXAccordionContent>
</PJXAccordion>
Props & API
Field Type Default Description
disabled bool False Marks the summary aria-disabled="true" + tabindex="-1" (and pointer-events: none via CSS).
class_name AttrValue "" Extra CSS class(es) for the root element.
content str \| BaseComponent "" Trigger label (text or rich markup).

Actions (opt-in). To add non-toggling action buttons, wrap them in <div class="pjx-accordion__actions"> inside content. pjx_accordion_trigger.js registers a single capture-phase click listener that calls preventDefault() (only — deliberately not stopPropagation()) for any click inside .pjx-accordion__actions. This cancels the native <summary> toggle while leaving htmx and other handlers intact.

DOM & classes

Root <summary class="pjx-accordion__trigger"> containing the auto-chevron and content. The chevron rotates on [open] via CSS. The default marker is stripped.

Classes: pjx-accordion__trigger, pjx-accordion__chevron, pjx-accordion__actions.

Python
PJXAccordionTrigger(content="What is pyjinhx?")

PJXAccordionContent

The body part of a composed accordion. Assets: none (unstyled wrapper; use your own or theme the parent shell).

<PJXAccordion>
  <PJXAccordionTrigger>What is pyjinhx?</PJXAccordionTrigger>
  <PJXAccordionContent><p>A Python/Jinja HTML component framework.</p></PJXAccordionContent>
</PJXAccordion>
Props & API
Field Type Default Description
class_name AttrValue "" Extra CSS class(es) for the root element.
content str \| BaseComponent "" Body content.
DOM & classes

Root <div class="pjx-accordion__content"> rendering {{ content }} verbatim.

Classes: pjx-accordion__content. No theming tokens — style via the parent pjx-accordion or your own rules.

Python
PJXAccordionContent(content="<p>A Python/Jinja HTML component framework.</p>")

PJXAccordionGroup

Groups a set of PJXAccordions into a shared layout/behavior container. Handles exclusive-open coordination in JS so you don't need to stamp a group= on every child. Assets: pjx_accordion_group.css, pjx_accordion_group.js.

<PJXAccordionGroup mode="exclusive" gap="0.25rem">
  <PJXAccordion>
    <PJXAccordionTrigger>Section A</PJXAccordionTrigger>
    <PJXAccordionContent><p>Content A.</p></PJXAccordionContent>
  </PJXAccordion>
  <PJXAccordion open="False">
    <PJXAccordionTrigger>Section B</PJXAccordionTrigger>
    <PJXAccordionContent><p>Content B.</p></PJXAccordionContent>
  </PJXAccordion>
</PJXAccordionGroup>
Props & API
Field Type Default Description
mode "exclusive" \| "multi" "multi" exclusive — at most one child <details> open at a time (JS-coordinated via toggle capture); multi — independent.
gap str "0" Space between accordion items; written to --pjx-accordion-group-gap inline on the root.
default_open "none" \| "first" \| "all" "none" Which items start open on mount. none — no change (each child respects its own open prop); first — JS opens the first direct <details> child; all — JS opens all direct <details> children. Applied by pjx_accordion_group.js after DOM settle.
content str \| BaseComponent "" Children (nested PJXAccordions or raw HTML).

default_open is JS-driven. The group receives its content as a pre-rendered string, so it cannot set the open attribute on individual child elements at the server level. Instead, data-default-open is emitted on the root div and pjx_accordion_group.js opens the appropriate children on mount (and after every htmx swap). Per-child open=True/False props are still respected when default_open="none".

JS is always loaded with the group asset. The script is a compact guarded IIFE; it exits early per-element when neither mode="exclusive" nor data-default-open requires action. In pure multi mode with default_open="none" (the defaults) the only runtime cost is the per-element data-pjx-group-init guard check.

Design decision — JS vs native <details name>. Native <details name="..."> provides exclusive-open without JS, but requires stamping the same name on every child element. PJXAccordion.group= exposes this as an explicit per-child opt-in (still supported, not deprecated). PJXAccordionGroup(mode="exclusive") is the composition-friendly alternative: the group's capture-phase toggle listener handles exclusive-open so callers don't set group= on every child. The two approaches are independent and can coexist.

Style tokens
Token Default
--pjx-accordion-group-gap 0
DOM & classes

Root <div class="pjx-accordion-group" data-pjx-accordion-group data-mode="..." [data-default-open="..."]>. pjx_accordion_group.js runs once per element (guarded by data-pjx-group-init), applies default_open, then wires the exclusive-open toggle listener when mode="exclusive". Re-initialized on htmx:afterSettle.

Classes: pjx-accordion-group.

Python
PJXAccordionGroup(
    mode="exclusive",
    gap="0.25rem",
    content=PJXAccordion(
        content=PJXAccordionTrigger(content="Section A").render()
        + PJXAccordionContent(content="<p>Content A.</p>").render()
    ).render()
    + PJXAccordion(
        open=False,
        content=PJXAccordionTrigger(content="Section B").render()
        + PJXAccordionContent(content="<p>Content B.</p>").render(),
    ).render(),
)

PJXTable

Accessible data table shell. Compose with PJXTableHead, PJXTableBody, PJXTableRow, PJXTableHeaderCell, and PJXTableCell parts. Assets: pjx_table.css.

<PJXTable caption="Team members" striped="true" bordered="horizontal">
  <PJXTableHead>
    <PJXTableRow>
      <PJXTableHeaderCell sortable="true" sort="asc">Name</PJXTableHeaderCell>
      <PJXTableHeaderCell>Role</PJXTableHeaderCell>
    </PJXTableRow>
  </PJXTableHead>
  <PJXTableBody>
    <PJXTableRow selectable="true" value="1"><PJXTableCell>Ada Lovelace</PJXTableCell><PJXTableCell>Engineer</PJXTableCell></PJXTableRow>
    <PJXTableRow selectable="true" value="2"><PJXTableCell>Alan Turing</PJXTableCell><PJXTableCell>Researcher</PJXTableCell></PJXTableRow>
  </PJXTableBody>
</PJXTable>
Props & API
Field Type Default Description
caption str "" <caption> text; suppressed when empty.
striped bool False Alternating row shading via pjx-table--striped.
sticky_header bool False Keeps the <thead> visible on scroll via pjx-table--sticky.
density "comfortable" \| "compact" "comfortable" Cell padding scale → pjx-table--density-compact when "compact"; no class for "comfortable" or "none".
bordered "none" \| "horizontal" \| "all" "none" Border style → pjx-table--bordered-{bordered} (omitted when "none").
class_name AttrValue "" Extra CSS class(es) on the root element.
content str \| BaseComponent "" Pre-rendered children (PJXTableHead + PJXTableBody).
Style tokens
Token Default
--pjx-table-border var(--border)
--pjx-table-header-bg var(--surface-alt)
--pjx-table-header-fg var(--text)
--pjx-table-stripe-bg var(--surface-alt)
--pjx-table-row-hover-bg var(--surface-alt)
--pjx-table-cell-pad-y 0.5rem
--pjx-table-cell-pad-x 0.75rem
DOM & classes

Root <table class="pjx-table"> with an optional <caption class="pjx-table__caption"> as its first child. No JS public API — sort state is reflected via aria-sort on <th> cells and driven by your own htmx/Alpine handlers on PJXTableHeaderCell.

Classes: pjx-table, pjx-table--striped, pjx-table--sticky, pjx-table--density-compact, pjx-table--bordered-horizontal, pjx-table--bordered-all.

Python
PJXTable(
    caption="Team members",
    striped=True,
    bordered="horizontal",
    content=(
        PJXTableHead(
            content=PJXTableRow(
                content=(
                    PJXTableHeaderCell(
                        sortable=True, sort="asc", content="Name"
                    ).render()
                    + PJXTableHeaderCell(content="Role").render()
                )
            ).render()
        ).render()
        + PJXTableBody(
            content=(
                PJXTableRow(
                    selectable=True,
                    value="1",
                    content=(
                        PJXTableCell(content="Ada Lovelace").render()
                        + PJXTableCell(content="Engineer").render()
                    ),
                ).render()
                + PJXTableRow(
                    selectable=True,
                    value="2",
                    content=(
                        PJXTableCell(content="Alan Turing").render()
                        + PJXTableCell(content="Researcher").render()
                    ),
                ).render()
            )
        ).render()
    ),
)

PJXTableHead

<thead> wrapper for a composed table. Assets: none (styled via pjx_table.css).

<PJXTable>
  <PJXTableHead>
    <PJXTableRow>
      <PJXTableHeaderCell>Name</PJXTableHeaderCell>
    </PJXTableRow>
  </PJXTableHead>
</PJXTable>
Props & API
Field Type Default Description
class_name AttrValue "" Extra CSS class(es) on the root element.
content str \| BaseComponent "" Pre-rendered PJXTableRow children.
DOM & classes

Root <thead class="pjx-table__head"> rendering {{ content }} verbatim.

Classes: pjx-table__head.

Python
PJXTableHead(
    content=PJXTableRow(content=PJXTableHeaderCell(content="Name").render()).render()
)

PJXTableBody

<tbody> wrapper for a composed table. Assets: none (styled via pjx_table.css).

<PJXTable>
  <PJXTableBody>
    <PJXTableRow><PJXTableCell>Ada Lovelace</PJXTableCell></PJXTableRow>
  </PJXTableBody>
</PJXTable>
Props & API
Field Type Default Description
class_name AttrValue "" Extra CSS class(es) on the root element.
content str \| BaseComponent "" Pre-rendered PJXTableRow children.
DOM & classes

Root <tbody class="pjx-table__body"> rendering {{ content }} verbatim.

Classes: pjx-table__body.

Python
PJXTableBody(
    content=PJXTableRow(content=PJXTableCell(content="Ada Lovelace").render()).render()
)

PJXTableRow

A <tr> for either the head or body section. When selectable=True, a checkbox cell is auto-prepended on the body row. Assets: none (styled via pjx_table.css).

<PJXTableRow selectable="true" value="1">
  <PJXTableCell>Ada Lovelace</PJXTableCell>
  <PJXTableCell>Engineer</PJXTableCell>
</PJXTableRow>
Props & API
Field Type Default Description
selectable bool False Auto-prepends a checkbox <td> with name="selected" and value="{{ value }}".
value str "" Checkbox value emitted when the row is selected.
select_label str "Select row" aria-label on the auto-prepended checkbox.
class_name AttrValue "" Extra CSS class(es) on the root element.
content str \| BaseComponent "" Pre-rendered cell children.

Select-all alignment. When using selectable rows, add a leading PJXTableHeaderCell to the head row (empty, or a select-all checkbox you wire with htmx/Alpine) so the columns align — selectable only auto-prepends a checkbox on body rows.

DOM & classes

Root <tr class="pjx-table__row">; pjx-table__row--selectable added when selectable=True.

Classes: pjx-table__row, pjx-table__row--selectable.

Python
PJXTableRow(
    selectable=True,
    value="1",
    content=PJXTableCell(content="Ada Lovelace").render()
    + PJXTableCell(content="Engineer").render(),
)

PJXTableHeaderCell

A <th> for use inside PJXTableRow in the head section. Reflects sort direction via aria-sort. Assets: none (styled via pjx_table.css).

<PJXTableHeaderCell sortable="true" sort="asc">Name</PJXTableHeaderCell>
Props & API
Field Type Default Description
sortable bool False Adds pjx-table__th--sortable and a sort-direction indicator.
sort "none" \| "asc" \| "desc" "none" Current sort direction → aria-sort attribute; "none" omits aria-sort.
class_name AttrValue "" Extra CSS class(es) on the root element.
content str \| BaseComponent "" Column header label.
DOM & classes

Root <th class="pjx-table__th" scope="col">. When sortable=True: pjx-table__th--sortable is added and aria-sort is set when sort is "asc" or "desc".

Classes: pjx-table__th, pjx-table__th--sortable.

Python
PJXTableHeaderCell(sortable=True, sort="asc", content="Name")

PJXTableCell

A <td> for use inside PJXTableRow in the body section. Assets: none (styled via pjx_table.css).

<PJXTableCell>Ada Lovelace</PJXTableCell>
Props & API
Field Type Default Description
class_name AttrValue "" Extra CSS class(es) on the root element.
content str \| BaseComponent "" Cell content.
DOM & classes

Root <td class="pjx-table__cell"> rendering {{ content }} verbatim.

Classes: pjx-table__cell.

Python
PJXTableCell(content="Ada Lovelace")

Overlays & dialogs

PJXModal

Native <dialog> shell. Compose with PJXModalHeader, PJXModalBody, and PJXModalFooter for structured layouts. Assets: pjx_modal.css, pjx_modal.js.

<PJXModal id="demo-modal">
  <PJXModalHeader id="demo-modal-h" title="Confirm changes"/>
  <PJXModalBody id="demo-modal-b">Your draft will be published immediately. This action cannot be undone.</PJXModalBody>
  <PJXModalFooter id="demo-modal-f"><button class="pjx-demo-btn" data-pjx-close>Cancel</button></PJXModalFooter>
</PJXModal>
Props & API
Field Type Default Description
content str \| BaseComponent "" Inner content (part components or arbitrary markup).
open_on_mount bool False When True, adds data-pjx-open-on-mount; JS opens the dialog as soon as it mounts (e.g. via hx-swap="beforeend").
remove_on_close bool False When True, adds data-pjx-remove-on-close; JS removes the element from the DOM on close.
class_name str "" Extra CSS classes on the root <dialog>.
Style tokens
Token Default
--pjx-modal-width 52rem
--pjx-modal-min-height 28rem
--pjx-modal-bg var(--surface)
--pjx-modal-border var(--border)
--pjx-modal-radius var(--radius-lg)
--pjx-modal-shadow var(--shadow-md)
--pjx-modal-header-bg var(--surface-alt)
--pjx-modal-header-sep var(--border)
--pjx-modal-footer-bg var(--surface-alt)
--pjx-modal-footer-sep var(--border)
--pjx-modal-title-color var(--text)
--pjx-modal-close-color var(--text-muted)
--pjx-modal-backdrop rgb(0 0 0 / 0.6)
--pjx-modal-padding 1.5rem

Close control hover uses var(--surface), var(--text), var(--radius-sm), var(--transition) from your theme.

DOM & classes

Root dialog.pjx-modal (state: [open], .pjx-modal--closing). Attributes: data-pjx-open="<id>" on any element opens it on click; data-pjx-close inside closes it; data-pjx-open-on-mount, data-pjx-remove-on-close reflect the lifecycle props. Events (bubble from the root): pjx:modal:before-open, pjx:modal:open, pjx:modal:before-close, pjx:modal:close* = cancelable, detail = {reason, trigger}, reason ∈ escape|backdrop|api|trigger. API: pjx.modal.open(id), pjx.modal.close(id).

Classes: pjx-modal; closing state pjx-modal--closing; pjx-modal__box. Part-component classes (__header, __title, __close, __body, __footer) belong to the respective part components below.

showModal() gives the dialog an implicit role="dialog" and aria-modal="true" — no extra markup needed. Content is arbitrary, so there's no automatic accessible name; pass aria-label, or aria-labelledby="<header-id>-title" pointing at your PJXModalHeader's title — both pass through to the root like any other attribute: <PJXModal aria-labelledby="demo-modal-h-title">.

Python
PJXModal(
    id="demo-modal",
    content=(
        PJXModalHeader(id="demo-modal-h", title="Confirm changes").render()
        + PJXModalBody(
            id="demo-modal-b",
            content="Your draft will be published immediately. This action cannot be undone.",
        ).render()
        + PJXModalFooter(
            id="demo-modal-f",
            content='<button class="pjx-demo-btn" data-pjx-close>Cancel</button>',
        ).render()
    ),
)

PJXModalHeader

Header part for PJXModal. Renders a <header> inside the modal box; always includes a close button. Assets: pjx_modal_header.css.

<PJXModalHeader title="Delete file?"/>
Props & API
Field Type Default Description
title str "" When set, renders <span id="{id}-title" class="pjx-modal__title"> with this text (escaped); when empty, {{ content }} is used instead. The id gives PJXModal's aria-labelledby something to point at.
close_label str "Close" aria-label for the auto-included close button.
close_content str "✕" Inner markup/text of the close button.
class_name str "" Extra CSS classes on the <header>.
content str \| BaseComponent "" Custom header body; used when title is empty.
DOM & classes

Classes: pjx-modal__header, pjx-modal__title, pjx-modal__close.

Python
PJXModalHeader(title="Delete file?")

PJXModalBody

Body part for PJXModal. Renders a <div class="pjx-modal__body">. Assets: pjx_modal_body.css.

<PJXModalBody>This cannot be undone.</PJXModalBody>
Props & API
Field Type Default Description
class_name str "" Extra CSS classes on the body <div>.
content str \| BaseComponent "" Main content.
DOM & classes

Classes: pjx-modal__body.

Python
PJXModalBody(content="This cannot be undone.")

PJXModalFooter

Footer part for PJXModal. Renders a <footer class="pjx-modal__footer">. Assets: pjx_modal_footer.css.

<PJXModalFooter><PJXButton content="Delete"/></PJXModalFooter>
Props & API
Field Type Default Description
class_name str "" Extra CSS classes on the <footer>.
content str \| BaseComponent "" Footer content (e.g. action buttons).
DOM & classes

Classes: pjx-modal__footer.

Python
PJXModalFooter(content=PJXButton(content="Delete").render())

PJXDrawer

<dialog> shell sheet from an edge. Assets: pjx_drawer.css, pjx_drawer.js (JS stays on the shell; each part ships its own CSS — see PJXDrawerHeader, PJXDrawerBody, PJXDrawerFooter).

<PJXDrawer id="demo-drawer" side="right">
  <PJXDrawerHeader id="demo-drawer-h" title="Filter results"/>
  <PJXDrawerBody id="demo-drawer-b"><p>Adjust filters to narrow down your results.</p></PJXDrawerBody>
  <PJXDrawerFooter id="demo-drawer-f"><button class="pjx-demo-btn" data-pjx-close>Done</button></PJXDrawerFooter>
</PJXDrawer>
Props & API
Field Type Default Description
side literal "right" left, right, or bottompjx-drawer--{side}.
open_on_mount bool False Adds data-pjx-open-on-mount; JS opens on arrival.
remove_on_close bool False Adds data-pjx-remove-on-close; JS removes element on close.
class_name str "" Extra CSS classes on the root <dialog>.
content str \| BaseComponent "" Inner slot; place composable part components here.
Style tokens
Token Default
--pjx-drawer-width min(24rem, 100vw)
--pjx-drawer-height-bottom min(50dvh, 28rem)
--pjx-drawer-bg var(--surface)
--pjx-drawer-border var(--border)
--pjx-drawer-shadow var(--shadow-md)
--pjx-drawer-backdrop rgb(0 0 0 / 0.45)
--pjx-drawer-header-bg var(--surface-alt)
--pjx-drawer-header-sep var(--border)
--pjx-drawer-footer-bg var(--surface-alt)
--pjx-drawer-footer-sep var(--border)
--pjx-drawer-padding 1rem
--pjx-drawer-z 250
DOM & classes

Root dialog.pjx-drawer.pjx-drawer--{side} (state: [open], .pjx-drawer--closing). data-pjx-open="<id>" on any element opens it; data-pjx-close inside closes it; data-pjx-open-on-mount, data-pjx-remove-on-close reflect lifecycle props. Events: pjx:drawer:before-open, pjx:drawer:open, pjx:drawer:before-close, pjx:drawer:close* = cancelable, detail = {reason, trigger}, reason ∈ escape|backdrop|api|trigger. API: pjx.drawer.open(id), pjx.drawer.close(id).

Classes: pjx-drawer; side modifiers pjx-drawer--left, --right, --bottom; closing state pjx-drawer--closing. The inner layout classes (__header, __title, __close, __body, __footer) belong to the part components — see below.

showModal() gives the dialog an implicit role="dialog" and aria-modal="true". Pass aria-label, or aria-labelledby="<header-id>-title" pointing at your PJXDrawerHeader's title — both pass through to the root: <PJXDrawer aria-labelledby="demo-drawer-h-title">.

Python
PJXDrawer(
    id="demo-drawer",
    side="right",
    content=(
        PJXDrawerHeader(id="demo-drawer-h", title="Filter results").render()
        + PJXDrawerBody(
            id="demo-drawer-b",
            content="<p>Adjust filters to narrow down your results.</p>",
        ).render()
        + PJXDrawerFooter(
            id="demo-drawer-f",
            content='<button class="pjx-demo-btn" data-pjx-close>Done</button>',
        ).render()
    ),
)

PJXDrawerHeader

Header bar for a PJXDrawer. Automatically includes a close button. Assets: pjx_drawer_header.css.

<PJXDrawerHeader title="Menu"/>
Props & API
Field Type Default Description
title str "" Renders <span id="{id}-title" class="pjx-drawer__title"> when set; omit to use content for a fully custom header. The id gives PJXDrawer's aria-labelledby something to point at.
close_label str "Close" aria-label for the auto-included close button.
close_content str "✕" Inner HTML/text of the close button.
class_name str "" Extra CSS classes on the root <header>.
content str \| BaseComponent "" Custom header slot used when title is not set.
DOM & classes

Root <header class="pjx-drawer__header">. When title is set renders <span class="pjx-drawer__title">. Always renders a <button class="pjx-drawer__close" data-pjx-close aria-label="...">.

Classes: pjx-drawer__header, pjx-drawer__title, pjx-drawer__close.

Python
PJXDrawerHeader(title="Menu")

PJXDrawerBody

Scrollable body region for a PJXDrawer. Assets: pjx_drawer_body.css.

<PJXDrawerBody>…links…</PJXDrawerBody>
Props & API
Field Type Default Description
class_name str "" Extra CSS classes on the root <div>.
content str \| BaseComponent "" Body slot.
DOM & classes

Root <div class="pjx-drawer__body">.

Classes: pjx-drawer__body.

Python
PJXDrawerBody(content="…links…")

PJXDrawerFooter

Footer strip for a PJXDrawer. Assets: pjx_drawer_footer.css.

<PJXDrawerFooter>v1.0</PJXDrawerFooter>
Props & API
Field Type Default Description
class_name str "" Extra CSS classes on the root <footer>.
content str \| BaseComponent "" Footer slot.
DOM & classes

Root <footer class="pjx-drawer__footer">.

Classes: pjx-drawer__footer.

Python
PJXDrawerFooter(content="v1.0")

PJXNotification

Fixed-position toast. Assets: pjx_notification.css, pjx_notification.js.

<PJXNotification
  id="demo-notification"
  content="Your changes have been saved."
  corner="top-right"
  timeout="4000"/>
Props & API
Field Type Default Description
content str \| BaseComponent "" Message body.
corner literal "top-right" top-right, top-left, bottom-right, bottom-left.
timeout int 5000 Auto-dismiss ms; 0 disables. Rendered as data-timeout.
autoshow bool True When True, adds data-pjx-autoshow; JS shows the notification as soon as it mounts.
dismiss_label str "Dismiss" aria-label for the dismiss button.

Maps children to content; see the children-vs-content note.

Style tokens
Token Default
--pjx-notification-width 22rem
--pjx-notification-gap 1.25rem (viewport inset; used in slide animations)
--pjx-notification-bg var(--surface-alt)
--pjx-notification-border var(--border)
--pjx-notification-radius var(--radius-md)
--pjx-notification-shadow var(--shadow-md)
--pjx-notification-padding 1rem 1rem 1rem 1.25rem
--pjx-notification-close-color var(--text-muted)
--pjx-notification-z 500

Content uses var(--font-size-sm), var(--text); close hover uses var(--surface), var(--text), var(--radius-sm), var(--transition).

DOM & classes

Root .pjx-notification (state: .pjx-notification--visible, .pjx-notification--hiding). data-pjx-autoshow triggers auto-show on mount. data-pjx-close inside hides it. Events: pjx:notification:before-show, pjx:notification:show, pjx:notification:before-hide, pjx:notification:hide* = cancelable, detail = {reason, trigger}. API: pjx.notification.show(id), pjx.notification.hide(id).

Classes: pjx-notification; placement pjx-notification--top-right, --top-left, --bottom-right, --bottom-left; JS state pjx-notification--visible, pjx-notification--hiding; pjx-notification__content, pjx-notification__close.

Python
PJXNotification(
    id="demo-notification",
    content="Your changes have been saved.",
    corner="top-right",
    timeout=4000,
)

PJXAlert

Inline status banner. Assets: pjx_alert.css, pjx_alert.js.

<PJXAlert variant="info" title="Heads up" body="A new version is available."/>
Props & API
Field Type Default Description
variant literal "info" info, success, warning, errorpjx-alert--{variant}.
title str "" Optional heading.
body str \| BaseComponent "" Main copy.
dismissible bool False When True, renders a dismiss button with data-pjx-close.
dismiss_label str "Dismiss" aria-label for the dismiss button.
Style tokens
Token Default
--pjx-alert-radius var(--radius-md)
--pjx-alert-padding 0.875rem 1rem
--pjx-alert-border var(--border)
--pjx-alert-shadow var(--shadow-md)
--pjx-alert-title-size var(--font-size-sm)
--pjx-alert-body-size var(--font-size-sm)
--pjx-alert-dismiss-color var(--text-muted)
DOM & classes

Root .pjx-alert (state: .pjx-alert--dismissed — set by JS, hides via display: none). data-pjx-close inside triggers dismissal. Events: pjx:alert:before-dismiss* (cancelable), pjx:alert:dismissdetail = {reason: 'trigger', trigger}.

Classes: pjx-alert; variant modifiers pjx-alert--info, --success, --warning, --error; dismissed state pjx-alert--dismissed; pjx-alert__inner, __text, __title, __body, __dismiss. Variants use color-mix with --brand, --success, --warning, or --error / --error-bg / --error-border where applicable.

Python
PJXAlert(variant="info", title="Heads up", body="A new version is available.")

PJXTooltip

Composable tooltip shell. Compose with PJXTooltipTrigger and PJXTooltipContent. Assets: pjx_tooltip.css, pjx_tooltip.js (IIFE — no API, behavior only).

<PJXTooltip id="demo-tooltip" placement="top">
  <PJXTooltipTrigger id="demo-tooltip-tr">Hover over me</PJXTooltipTrigger>
  <PJXTooltipContent id="demo-tooltip-tc">This is additional context shown on hover or focus.</PJXTooltipContent>
</PJXTooltip>
Props & API
Field Type Default Description
placement literal "top" top, bottom, start, enddata-pjx-tooltip-placement.
backdrop bool False Dim the rest of the page while the tip is open (attention-grabbing hover cards).
content str \| BaseComponent "" Children: compose PJXTooltipTrigger + PJXTooltipContent here.
Style tokens
Token Default
--pjx-tooltip-gap 6px
--pjx-tooltip-bg var(--surface-alt)
--pjx-tooltip-border var(--border)
--pjx-tooltip-radius var(--radius-sm)
--pjx-tooltip-shadow var(--shadow-md)
--pjx-tooltip-padding 0.35rem 0.5rem
--pjx-tooltip-max-width 20ch
--pjx-tooltip-fg var(--text)
--pjx-tooltip-font-size var(--font-size-xs)
--pjx-tooltip-z 400
--pjx-tooltip-backdrop rgb(0 0 0 / 0.45)
--pjx-tooltip-backdrop-z 390
DOM & classes

Root .pjx-tooltip <span>. data-pjx-tooltip-placement drives JS positioning. Tip shows on mouseover anywhere inside .pjx-tooltip root, or on focusin of .pjx-tooltip__trigger; hides on mouseout/focusout; repositions on scroll. The JS sets aria-describedby at runtime when the tip is shown. No JS API (pjx._tooltipWired guard only).

With backdrop=True the root also renders a .pjx-tooltip__backdrop <span> (data-pjx-tooltip-backdrop, hidden) that the same show/hide lifecycle fades in and out via .pjx-tooltip__backdrop--visible. It is position: fixed and pointer-events: none, sits below the tip at --pjx-tooltip-backdrop-z, and lifts .pjx-tooltip__trigger above itself while open.

Classes: pjx-tooltip, pjx-tooltip__backdrop, pjx-tooltip__backdrop--visible.

Python
PJXTooltip(
    id="demo-tooltip",
    placement="top",
    content=(
        PJXTooltipTrigger(id="demo-tooltip-tr", content="Hover over me").render()
        + PJXTooltipContent(
            id="demo-tooltip-tc",
            content="This is additional context shown on hover or focus.",
        ).render()
    ),
)

PJXTooltipTrigger

The focusable trigger part of a tooltip. Assets: pjx_tooltip_trigger.css.

<PJXTooltipTrigger>Hover over me</PJXTooltipTrigger>
Props & API
Field Type Default Description
content str \| BaseComponent "" Trigger label or element.
DOM & classes

<span class="pjx-tooltip__trigger" tabindex="0">. The JS sets aria-describedby to the tip's id at show time. Place inside a PJXTooltip shell.

Classes: pjx-tooltip__trigger.

Python
PJXTooltipTrigger(content="Hover over me")

PJXTooltipContent

The floating tip body. Assets: pjx_tooltip_content.css.

<PJXTooltipContent>This is additional context shown on hover or focus.</PJXTooltipContent>
Props & API
Field Type Default Description
content str \| BaseComponent "" Tooltip text or rich content.
DOM & classes

<span class="pjx-tooltip__tip" role="tooltip" hidden>. The JS removes hidden and adds .pjx-tooltip__tip--visible on show. Place inside a PJXTooltip shell.

Classes: pjx-tooltip__tip.

Python
PJXTooltipContent(content="This is additional context shown on hover or focus.")

PJXPopover

Click-toggle compound. Three separate components; compose them by placing PJXPopoverTrigger and PJXPopoverPanel as children inside PJXPopover. Assets: pjx_popover.css, pjx_popover.js (IIFE under pjx.popover).

<PJXPopover id="demo-popover">
  <PJXPopoverTrigger id="demo-popover-t" role="dialog">Show info</PJXPopoverTrigger>
  <PJXPopoverPanel id="demo-popover-p" role="dialog"><strong>Keyboard shortcuts</strong><p style="margin:.35rem 0 0">Press <kbd>?</kbd> anytime to reopen this panel.</p></PJXPopoverPanel>
</PJXPopover>
Props & API
Field Type Default Description
content str \| BaseComponent "" Children (trigger + panel).
align literal "start" start or end (panel alignment) → pjx-popover--align-end.
behavior bool True When True, adds data-pjx-popover (JS picks it up).
portal bool False Reparent the panel to document.body while open, so an overflow: hidden/auto ancestor can't clip it.
Style tokens
Token Default
--pjx-popover-max-width 28ch
--pjx-popover-bg var(--surface-alt)
--pjx-popover-border var(--border)
--pjx-popover-radius var(--radius-md)
--pjx-popover-shadow var(--shadow-md)
--pjx-popover-padding var(--space-3, 0.75rem) var(--space-4, 1rem)
--pjx-popover-z 300
DOM & classes

Root [data-pjx-popover] (the PJXPopover wrapper). Trigger: [data-pjx-toggle] on the trigger element; aria-expanded synced by JS. Panel: [data-pjx-popover-panel] element, hidden when closed. data-pjx-close inside the panel closes it. data-pjx-toggle="<panel-id>" on any element opens/closes a named panel. Events (bubble from [data-pjx-popover]): pjx:popover:before-open, pjx:popover:open, pjx:popover:before-close, pjx:popover:closedetail = {reason, trigger}. API: pjx.popover.open(idOrEl), pjx.popover.close(idOrEl), pjx.popover.toggle(idOrEl).

With portal=True the root also carries data-pjx-popover-portal, and opening moves the panel alone to document.body, restoring it to its original position on close — the same escape hatch PJXTooltip's portal prop offers when an ancestor's overflow clips the panel. The root, trigger, and panel keep resolving to each other while the panel lives elsewhere, so events and the JS API are unchanged. Two consequences are specific to a portalled popover: scrolling anywhere outside the panel closes it (a panel parked on document.body would otherwise detach visually from a trigger scrolling out from under it), emitting the usual close events with reason: "scroll"; and if the panel's original parent is removed from the document while the panel is open, the panel is dropped rather than left orphaned on document.body.

Classes: pjx-popover, pjx-popover--align-end, pjx-popover__trigger, pjx-popover__panel.

Python
PJXPopover(
    id="demo-popover",
    content=(
        PJXPopoverTrigger(
            id="demo-popover-t", content="Show info", role="dialog"
        ).render()
        + PJXPopoverPanel(
            id="demo-popover-p",
            role="dialog",
            content=(
                "<strong>Keyboard shortcuts</strong>"
                '<p style="margin:.35rem 0 0">Press <kbd>?</kbd> anytime to reopen this panel.</p>'
            ),
        ).render()
    ),
)

PJXPopoverTrigger

Trigger button for a PJXPopover. Assets: bundled in pjx_popover.css.

<PJXPopoverTrigger id="t">Open</PJXPopoverTrigger>
Props & API
Field Type Default Description
content str \| BaseComponent "" Button/div label.
tag literal "button" button or div (role="button").
role literal "" aria-haspopup value: menu, listbox, dialog, or "".
behavior bool True When True, adds data-pjx-toggle.
DOM & classes

[data-pjx-toggle] element; aria-expanded synced by JS.

Classes: pjx-popover__trigger.

Python
PJXPopoverTrigger(id="t", content="Open")

PJXPopoverPanel

Floating panel for a PJXPopover. Assets: bundled in pjx_popover.css.

<PJXPopoverPanel id="p" role="menu"></PJXPopoverPanel>
Props & API
Field Type Default Description
content str \| BaseComponent "" Panel body.
as_form bool False Render panel as <form> instead of <div>.
role literal "" ARIA role attribute (menu, listbox, dialog, or "").
behavior bool True When True, adds data-pjx-popover-panel (hidden by default).
DOM & classes

[data-pjx-popover-panel] element, hidden when closed.

Classes: pjx-popover__panel.

Python
PJXPopoverPanel(id="p", role="menu", content="…")

PJXSelect

A styled replacement for a bare <select>, single-choice or multi. A hidden native <select> carries the same options, so a plain form submit still posts a value without JS. Assets: pjx_select.css, pjx_select.js.

<PJXSelect name="fruit" ... />
Props & API
Field Type Default Description
name str Required. Form field name, on both the root (data-name) and the native <select>.
options list[SelectOption] Required. Each SelectOption has value and label. Plain dicts are coerced.
value str \| list[str] \| None None Current selection. Must be a list when multiple=True, a str otherwise; a value matching no option renders unselected.
multiple bool False Multi-select: checkbox per row, chip summary on the trigger.
placeholder str "Select…" Trigger text when nothing is selected.
disabled bool False Marks the root, the trigger, the native <select>, and the filter input.
portal bool False Reparent the whole root to document.body while open, so an overflow: hidden/auto ancestor can't clip the panel.
class_name AttrValue "" Appended to the root's classes.

Panels with more than 8 options render a search input above the list; shorter lists render without one. The input is UI-only — it hides option buttons and never posts. Opening focuses that input when present, for both mouse and keyboard opens.

Style tokens
Token Default
--pjx-select-bg var(--surface, #fff)
--pjx-select-fg var(--text)
--pjx-select-border var(--border)
--pjx-select-focus var(--border-focus, var(--border))
--pjx-select-radius var(--radius-md)
--pjx-select-padding 0.375rem 0.5rem
--pjx-select-panel-bg var(--surface-alt)
--pjx-select-panel-shadow var(--shadow-md)
--pjx-select-panel-min-w 12rem
--pjx-select-panel-max-h 16rem
--pjx-select-panel-z 300
--pjx-select-option-hover var(--surface-hover, var(--surface-alt))
--pjx-select-option-active var(--accent-soft, var(--surface-alt))
DOM & classes

Root [data-pjx-select], carrying data-name, plus data-multiple and data-placeholder in multi mode, data-disabled when disabled, and data-pjx-select-portal when portal=True. Trigger: [data-pjx-select-trigger] with aria-haspopup="listbox" and aria-expanded synced by JS. Panel: [data-pjx-select-panel] with role="listbox", hidden when closed; opening always sets its width to match the trigger's rendered width and writes inline left/top on top of that only when the panel would otherwise overflow the viewport (flip/clamp), removing everything on close. Options: [data-pjx-select-option] with data-value, role="option", and aria-selected as the selection state. Filter: [data-pjx-select-filter], presentation-only — it hides option buttons and never changes selection; opening focuses it when present, from either a mouse or a keyboard open. A hidden native <select name="…"> inside the root is the form's source of truth and is re-derived from the option buttons on every change, dispatching real change/input events on itself so hx-trigger="change from:select" and vanilla listeners still fire; without JS it submits on its own.

With portal=True, opening reparents the whole root (not just the panel) to document.body, fixed at its pre-move screen position so the trigger doesn't visibly jump, and moves it back to its original parent on close — the same escape hatch PJXTooltip's portal prop offers for a clipping overflow: hidden/auto ancestor.

Keyboard: ArrowDown/ArrowUp/Home/End move focus among options (wrapping); ArrowDown/ArrowUp/Enter/Space on the trigger open the panel and focus the filter input if present, otherwise an edge option; type-ahead jumps to the next option whose label starts with the typed prefix (case-insensitive, buffer resets after a pause); Enter/Space selects and closes in single mode, toggles and stays open in multi mode; Escape closes and returns focus to the trigger from anywhere inside the root, including the filter input.

Classes: pjx-select, pjx-select__trigger, pjx-select__label, pjx-select__chips, pjx-select__panel, pjx-select__filter, pjx-select__option, pjx-select__checkbox. Chips reuse pjx-chip-input__chip / pjx-chip-input__label.

Python
PJXSelect(
    name="fruit",
    options=[
        SelectOption(value="a", label="Apple"),
        SelectOption(value="b", label="Banana"),
    ],
    value=["a", "b"],
    multiple=True,
)

Interaction & loading

PJXDropdown

Button + anchored panel backed by the shared popover engine. Assets: pjx_dropdown.css plus pjx_popover.js, inherited from the PJXPopover it extends — a rendered dropdown ships the runtime that reads its data-pjx-popover markup.

<!-- Menu entries are supplied via the `items` prop (a list); see the Python example below. -->
<PJXDropdown trigger="Actions" menu_label="Actions menu" />
Props & API
Field Type Default Description
trigger str \| BaseComponent "" Button label.
items list[str \| BaseComponent] [] Menu items. A str entry is plain text and is HTML-escaped; pass a component for markup.
align literal "start" start or endpjx-dropdown--align-end.
menu_label str "Submenu" aria-label on the menu panel.
portal bool False Inherited from PJXPopover: reparent the menu to document.body while open, escaping a clipping ancestor.

Trigger id is {{ id }}-trigger, menu is {{ id }}-menu.

Style tokens
Token Default
--pjx-dropdown-menu-bg var(--surface-alt)
--pjx-dropdown-menu-border var(--border)
--pjx-dropdown-menu-radius var(--radius-md)
--pjx-dropdown-menu-shadow var(--shadow-md)
--pjx-dropdown-menu-padding 0.35rem 0
--pjx-dropdown-menu-min-w 10rem
--pjx-dropdown-menu-max-h min(70dvh, 24rem)
--pjx-dropdown-z 350
--pjx-dropdown-trigger-bg var(--surface-alt)
--pjx-dropdown-trigger-border var(--border)
--pjx-dropdown-trigger-radius var(--radius-md)
--pjx-dropdown-trigger-padding 0.5rem 0.85rem
--pjx-dropdown-trigger-bg-hover color-mix(in srgb, var(--text) 6%, var(--surface-alt))
DOM & classes

Root .pjx-dropdown with data-pjx-popover. Trigger: button.pjx-dropdown__trigger with data-pjx-toggle="{{ id }}-menu", aria-expanded synced by pjx_popover.js. Panel: div.pjx-dropdown__menu[data-pjx-popover-panel][role="menu"], hidden when closed. All popover events and API apply: pjx.popover.open/close/toggle(panelId). Document click outside closes the menu; Escape closes all open popovers. portal=True behaves exactly as it does on PJXPopover, menu in place of panel.

Classes: pjx-dropdown, pjx-dropdown--align-end, pjx-dropdown__trigger, pjx-dropdown__menu.

Python
PJXDropdown(
    trigger="Actions",
    items=[
        PJXButton(variant="ghost", content=label)
        for label in ("Edit", "Duplicate", "Delete")
    ],
    menu_label="Actions menu",
)

PJXTabGroup

Thin shell that wraps a composed tab layout. Compose with PJXTabList, PJXTab, and PJXTabPanel. Assets: pjx_tab_group.css, pjx_tab_group.js.

<PJXTabGroup>
  <PJXTabList>
    <PJXTab id="tg-t0" panel="tg-p0" selected="true">Overview</PJXTab>
    <PJXTab id="tg-t1" panel="tg-p1">Activity</PJXTab>
    <PJXTab id="tg-t2" panel="tg-p2" closeable="true">Settings</PJXTab>
  </PJXTabList>
  <PJXTabPanel id="tg-p0" tab="tg-t0"><p>Project summary and key metrics.</p></PJXTabPanel>
  <PJXTabPanel id="tg-p1" tab="tg-t1"><p>Recent commits and deploys.</p></PJXTabPanel>
  <PJXTabPanel id="tg-p2" tab="tg-t2"><p>Repository configuration.</p></PJXTabPanel>
</PJXTabGroup>
Props & API
Field Type Default Description
class_name str "" Extra CSS class(es) appended to the root element.
content str \| BaseComponent "" Composed interior — typically a PJXTabList followed by one or more PJXTabPanels.

Panel mode (standalone triggers). A PJXTab rendered outside a PJXTabList becomes a free-standing trigger: it gets button semantics and an aria-current active state instead of the tab strip, and it can live anywhere on the page. It pairs with its region by panel= (→ aria-controls); the engine resolves the trigger's group through the panel it controls. This replaces the former PJXPanel/PJXPanelTrigger pair. The PJXTab wrapper itself is the interactive element (role="button", tabindex="0"), so its content should be inert — plain text or a <span> — exactly like list-mode tabs. The engine also backfills aria-labelledby on each panel at init from its controlling trigger, so tab= on PJXTabPanel is not required in panel mode.

<PJXTab panel="files-panel" selected="true">Files</PJXTab>
<PJXTab panel="chat-panel">Chat</PJXTab>

<PJXTabGroup id="workspace">
  <PJXTabPanel id="files-panel"><p>Uploaded assets.</p></PJXTabPanel>
  <PJXTabPanel id="chat-panel"><p>Team conversations.</p></PJXTabPanel>
</PJXTabGroup>
Style tokens
Token Default
--pjx-tab-group-border var(--border)
--pjx-tab-group-bg var(--surface-alt)
--pjx-tab-group-tab-active-fg var(--text)
--pjx-tab-group-tab-active-bg color-mix(in srgb, var(--surface) 55%, var(--surface-alt))
--pjx-tab-group-tab-active-border var(--brand)
--pjx-tab-group-panel-bg var(--surface)
DOM & classes

Root div.pjx-tab-group[data-pjx-tab-group]. The interior is authored by the caller via content. pjx_tab_group.js delegates events on .pjx-tab-group[data-pjx-tab-group].

Classes: pjx-tab-group.

Python
PJXTabGroup(
    content=(
        PJXTabList(
            content=(
                PJXTab(
                    id="tg-t0", panel="tg-p0", selected=True, content="Overview"
                ).render()
                + PJXTab(id="tg-t1", panel="tg-p1", content="Activity").render()
                + PJXTab(
                    id="tg-t2", panel="tg-p2", closeable=True, content="Settings"
                ).render()
            )
        ).render()
        + PJXTabPanel(
            id="tg-p0", tab="tg-t0", content="<p>Project summary and key metrics.</p>"
        ).render()
        + PJXTabPanel(
            id="tg-p1", tab="tg-t1", content="<p>Recent commits and deploys.</p>"
        ).render()
        + PJXTabPanel(
            id="tg-p2", tab="tg-t2", content="<p>Repository configuration.</p>"
        ).render()
    ),
)

PJXTabList

Tab button list (the role="tablist" container). Assets: pjx_tab_list.css.

<PJXTabList label="Project tabs">
  <PJXTab id="t0" panel="p0" selected="true">Overview</PJXTab>
  <PJXTab id="t1" panel="p1">Activity</PJXTab>
</PJXTabList>
Props & API
Field Type Default Description
label str "Tabs" aria-label for the tab list — describes the set of tabs.
reorderable bool False Adds data-pjx-tab-reorderable; non-pinned tabs become drag-reorderable, and a focused tab can be moved with Ctrl+ArrowLeft/Ctrl+ArrowRight.
class_name str "" Extra CSS class(es) appended to the root element.
content str \| BaseComponent "" One or more PJXTab rendered strings.
DOM & classes

Root div[role="tablist"][aria-label][aria-orientation="horizontal"].pjx-tab-group__list (reorderable: [data-pjx-tab-reorderable]).

Reordering (drag, or Ctrl+Arrow on a focused tab) fires pjx:tab:before-reorder* then pjx:tab:reorder on the group root — * = cancelable, detail = {id, from, to} (indices among the tablist's own tabs). A PJXTab(pinned=true) is excluded from reordering.

Python
PJXTabList(
    label="Project tabs",
    content=PJXTab(id="t0", panel="p0", selected=True, content="Overview").render()
    + PJXTab(id="t1", panel="p1", content="Activity").render(),
)

PJXTab

Single tab button. Assets: pjx_tab.css.

<PJXTab id="t0" panel="p0" selected="true" icon="file">Overview</PJXTab>
<PJXTab id="t1" panel="p1" closeable="true">Activity</PJXTab>
Props & API
Field Type Default Description
panel str "" id of the associated PJXTabPanel (aria-controls).
icon str "" Icon name passed to PJXIcon; shown before the label.
closeable bool False Renders a close button (data-pjx-tab-close).
pinned bool False Marks the tab as pinned; suppresses close button.
selected bool False Initially selected state (aria-selected, tabindex=0).
close_label str "Close" aria-label for the close button.
class_name str "" Extra CSS class(es) appended to the root element.
content str \| BaseComponent "" Tab label text or markup.
DOM & classes

Root div[role="tab"][data-pjx-tab].pjx-tab. Selected state adds pjx-tab--selected; closeable adds pjx-tab--closeable; pinned adds pjx-tab--pinned. The id here must match the tab field on the corresponding PJXTabPanel.

Python
PJXTab(id="t0", panel="p0", selected=True, icon="file", content="Overview")

PJXTabPanel

Panel body associated with one tab. Assets: pjx_tab_panel.css.

<PJXTabPanel id="p0" tab="t0"><p>Panel content.</p></PJXTabPanel>
Props & API
Field Type Default Description
tab str "" id of the owning PJXTab (aria-labelledby).
class_name str "" Extra CSS class(es) appended to the root element.
content str \| BaseComponent "" Panel body HTML or component.
DOM & classes

Root div[role="tabpanel"][data-pjx-region][hidden].pjx-tab-group__panel. hidden is present on all panels; JS removes it from the selected one. aria-labelledby points to the owning tab's id.

Python
PJXTabPanel(id="p0", tab="t0", content="<p>Panel content.</p>")

PJXResizableGroup

Drag-to-resize split-pane container. Compose PJXResizablePanel and PJXResizableHandle parts inside. Assets: pjx_resizable_group.css, pjx_resizable_group.js.

<PJXResizableGroup direction="row">
  <PJXResizablePanel size="25" min="15">sidebar</PJXResizablePanel>
  <PJXResizableHandle/>
  <PJXResizablePanel size="75">main</PJXResizablePanel>
</PJXResizableGroup>
Props & API
Field Type Default Description
direction "row" \| "column" "row" Flex direction for the group (row = side-by-side, column = stacked). Emits pjx-resizable-group--row / --column and data-direction.
class_name AttrValue "" Extra CSS class(es) on the root element.
content str \| BaseComponent "" Pre-rendered children: alternating PJXResizablePanel and PJXResizableHandle components.

pjx:resize event. Fires on the group root after every drag-end or keyboard step. detail.sizes is an ordered array of panel size percentages (one entry per [data-pjx-resizable-panel] direct child). Listen to persist the layout:

document.addEventListener("pjx:resize", e => {
    localStorage.setItem("panel-sizes", JSON.stringify(e.detail.sizes));
});

Nesting. A PJXResizablePanel may contain a PJXResizableGroup of the perpendicular direction, giving a resizable grid. Each group initializes independently via :scope + direct-children traversal.

Style tokens
Token Default
--pjx-resizable-handle-size 0.5rem
--pjx-resizable-handle-color transparent
--pjx-resizable-grip-color var(--border)
--pjx-resizable-handle-color-active var(--brand-muted)
DOM & classes

Root <div role="group" class="pjx-resizable-group pjx-resizable-group--{direction}" data-pjx-resizable-group data-direction="{direction}"> rendering {{ content }} verbatim. pjx_resizable_group.js initializes on DOMContentLoaded and htmx:afterSettle (bind-guarded per element), sets flex-grow on each panel from the panel's data-size attribute, and wires pointer/touch drag and keyboard events to the handles.

Classes: pjx-resizable-group; direction modifiers pjx-resizable-group--row, --column; drag state pjx-resizable-group--dragging.

Python
PJXResizableGroup(
    id="split",
    direction="row",
    content=(
        PJXResizablePanel(id="split-l", size=25, min=15, content="sidebar").render()
        + PJXResizableHandle(id="split-h").render()
        + PJXResizablePanel(id="split-r", size=75, content="main").render()
    ),
)

PJXResizablePanel

A resizable pane inside a PJXResizableGroup. Percentage-sized; flex-grow is set from data-size by JS. Assets: pjx_resizable_panel.css.

<PJXResizableGroup direction="row">
  <PJXResizablePanel size="40" min="20">Left panel</PJXResizablePanel>
  <PJXResizableHandle/>
  <PJXResizablePanel size="60">Right panel</PJXResizablePanel>
</PJXResizableGroup>
Props & API
Field Type Default Description
size float \| None None Initial size as a percentage of the group (e.g. 40 = 40 %). Noneflex-grow: 1 (equal share). Rendered as data-size and inline style="flex-grow: {size}".
min str \| float 0 Minimum size. A number = percent of the group; "120px" = pixels; "content" = the panel's intrinsic min-content (so a fixed child stays visible at any viewport).
max str \| float 100 Maximum size. A number = percent; "120px" = pixels. ("content" is not valid for max.)
class_name AttrValue "" Extra CSS class(es) on the root element.
content str \| BaseComponent "" Panel body content.

Content floor — keeps the bottom strip visible regardless of viewport height:

<PJXResizableGroup direction="column">
  <PJXResizablePanel size="70">messages</PJXResizablePanel>
  <PJXResizableHandle/>
  <PJXResizablePanel size="30" min="content">workspace strip</PJXResizablePanel>
</PJXResizableGroup>
DOM & classes

Root <div class="pjx-resizable-group__panel" data-pjx-resizable-panel [data-size] data-min data-max style="flex-grow: ...">. Place inside a PJXResizableGroup; a PJXResizableHandle between two panels enables drag-to-resize. Each panel in the group must be a direct child.

Classes: pjx-resizable-group__panel. No own theming tokens; layout is controlled by flex-grow.

Python
PJXResizablePanel(size=40, min=20, content="Left panel")

PJXResizableHandle

The draggable divider between two PJXResizablePanel siblings. Keyboard-accessible (role="separator", tabindex="0"). Assets: pjx_resizable_handle.css.

<PJXResizableGroup direction="row">
  <PJXResizablePanel size="50">Left</PJXResizablePanel>
  <PJXResizableHandle label="Resize panels"/>
  <PJXResizablePanel size="50">Right</PJXResizablePanel>
</PJXResizableGroup>
Props & API
Field Type Default Description
label str "Resize" aria-label for the role="separator" element.
class_name AttrValue "" Extra CSS class(es) on the root element.

Keyboard controls. When the handle is focused: ArrowRight/ArrowDown grows the preceding panel by 5 %; ArrowLeft/ArrowUp shrinks it by 5 %; Home collapses the preceding panel to its min; End expands it to the remaining group space (clamped to max). min/max constraints from both neighbors are respected.

DOM & classes

Root <div role="separator" class="pjx-resizable-group__handle" data-pjx-resizable-handle tabindex="0" aria-label="{label}" aria-valuemin="0" aria-valuemax="100">. Place between two PJXResizablePanels inside a PJXResizableGroup. pjx_resizable_group.js sets aria-valuenow on the preceding panel's size at every update.

Classes: pjx-resizable-group__handle. Handle appearance is controlled by the PJXResizableGroup style tokens.

Python
PJXResizableHandle(label="Resize panels")

PJXToastHost

HX-Trigger-driven toast container singleton. Mount once in the layout. Assets: pjx_toast_host.css, pjx_toast_host.js.

<PJXToastHost position="bottom-right"/>
Props & API
Field Type Default Description
position literal "bottom-right" top-right, top-left, bottom-right, bottom-left.
timeout int 4000 Default auto-dismiss ms; 0 disables.
dismiss_label str "Dismiss" aria-label for dismiss buttons on individual toasts.
event_name str "pjx:toast" Custom event name to listen for (wired on mount).

Fire from a FastAPI route via HX-Trigger:

import json

response.headers["HX-Trigger"] = json.dumps(
    {"pjx:toast": {"message": "Saved.", "level": "success"}}
)

Or from JS:

pjx.toast("Upload complete.", { level: "success", timeout: 3000 });

Toast level maps to .pjx-toast--<level>; supported values: info, success, warning, error.

Style tokens
Token Default
--pjx-toast-bg var(--surface)
--pjx-toast-border var(--border)
--pjx-toast-radius var(--radius-md)
--pjx-toast-shadow var(--shadow-md)
--pjx-toast-gap 0.5rem
--pjx-toast-z 1000
--pjx-toast-info var(--brand, #5c8fa8)
--pjx-toast-success #3e7d4f
--pjx-toast-warning #b07415
--pjx-toast-error #b3261e
DOM & classes

Root div.pjx-toast-host[data-pjx-toast-host]. data-event-name sets the custom event; data-timeout sets the default dismiss timeout; data-dismiss-label sets dismiss button label. Events (bubble from the host): pjx:toasthost:show (detail: {level}), pjx:toasthost:hide. API: pjx.toast(message, {level?, timeout?}). Individual toasts: div.pjx-toast.pjx-toast--<level> > .pjx-toast__message + button.pjx-toast__dismiss.

Classes: pjx-toast-host, pjx-toast-host--top-right, --top-left, --bottom-right, --bottom-left; pjx-toast, pjx-toast--info, --success, --warning, --error; pjx-toast--hiding; pjx-toast__message, pjx-toast__dismiss.

Python
PJXToastHost(position="bottom-right")

PJXRegionLoader

In-place loading veil over a positioned ancestor. Assets: pjx_region_loader.css, pjx_region_loader.js.

<PJXRegionLoader id="demo-region"/>
Props & API
Field Type Default Description
aria_label str "Loading" Accessible label (role="status").

Layout: Overlay is position: absolute; inset: 0. Parent must be position: relative (or any non-static value) so coverage is correct.

Supports both declarative (hx-indicator) and programmatic use (pjx.loader.region.*).

Style tokens
Token Default
--pjx-region-loader-bg color-mix(in srgb, var(--text) 55%, transparent)
--pjx-region-loader-backdrop blur(2px)
--pjx-region-loader-z 100
--pjx-region-loader-spinner-size 2.5rem
--pjx-region-loader-spinner-width 3px
--pjx-region-loader-spinner-color var(--brand)
--pjx-region-loader-spinner-track color-mix(in srgb, var(--text) 12%, transparent)
DOM & classes

Root .pjx-region-loader (state: .pjx-region-loader--visible, .pjx-region-loader--hiding; also responds to .htmx-request as an htmx indicator — CSS activates the veil, no JS call required). Events (non-cancelable): pjx:region-loader:show, pjx:region-loader:hide. API: pjx.loader.region.show/hide/reset(id) and pjx.loader.region.wrap(id, promise). Ref-counted for concurrent sources: visible from the first show(id) to the last hide(id); show/hide events fire only on real visibility transitions (a show during an in-flight hide cancels it silently); hides finalize via a fallback timer even when animations are disabled. wrap(id, promise) pairs show/hide around any async task. Nodes replaced by a swap while sources remain active are re-lit on htmx:afterSettle.

Classes: pjx-region-loader; state pjx-region-loader--visible, pjx-region-loader--hiding; pjx-region-loader__spinner.

Python
PJXRegionLoader(id="demo-region")

PJXPageLoader

Full-page navigation loader. Mount once at the top of the layout body. Assets: pjx_page_loader.css, pjx_page_loader.js.

<PJXPageLoader id="demo-page-loader"/>
Props & API
Field Type Default Description
nav_targets str "app-content" Comma-separated element ids whose htmx GET requests activate the loader.
active_on_load bool True When True, renders with .pjx-page-loader--active (cold-load shimmer, removed on DOMContentLoaded).
loading_label str "Loading" aria-label for the role="status" element.

Add data-pjx-loader to any element to make its htmx requests activate the loader regardless of nav_targets:

<a hx-get="/slow-page" hx-target="#main-content" data-pjx-loader>Go</a>
Style tokens
Token Default
--pjx-page-loader-backdrop rgb(0 0 0 / 0.15)
--pjx-page-loader-z 9999
--pjx-page-loader-size 2rem
DOM & classes

Root div.pjx-page-loader[data-pjx-page-loader] (state: .pjx-page-loader--active). data-nav-targets — comma-separated ids; htmx GET requests targeting any of these activate the loader. data-pjx-loader on any element marks its htmx requests as loader-tracked regardless of target. Tracking is detected via htmx:beforeRequest; the loader releases via the request's loadend (terminal on load, error, abort, and timeout); history navigations reset via htmx:historyRestore. Events (non-cancelable, bubble from the root): pjx:page-loader:show, pjx:page-loader:hide. API: pjx.pageLoader.show(), pjx.pageLoader.hide(), pjx.pageLoader.reset(), pjx.pageLoader.wrap(promise).

Classes: pjx-page-loader, pjx-page-loader--active, pjx-page-loader__spinner.

Python
PJXPageLoader(id="demo-page-loader")

PJXLazyLoad

HTMX deferred-content loader: an element that fetches url on a computed trigger and swaps itself with the response. Use it three ways — a lazy panel (load a section's content on first reveal), load-on-mount, or an infinite-scroll sentinel at the end of a list/table. Assets: pjx_lazy_load.css, pjx_lazy_load.js. PJXLazyLoad needs a backend, so it has no live render.

<PJXLazyLoad id="comments" url="/posts/42/comments">
  <PJXSkeleton id="comments-skel"/>
</PJXLazyLoad>
Props & API
Field Type Default Description
url str required Endpoint for the deferred content (hx-get).
when literal "viewport" viewport (scroll-revealed), reveal (pjx:reveal from nearest [data-pjx-region]), load (immediately). Overridden by trigger.
trigger str "" Explicit hx-trigger value; overrides when entirely when set.
swap str "outerHTML" hx-swap strategy. The default self-replaces the element.
tag literal "div" Rendered root element: div, li (inside a <ul>/<ol>), or tr (inside a <tbody>). Lets the loader sit directly in a list or table.
content str \| BaseComponent "" Placeholder / loading indicator shown until the fetch lands (e.g. a PJXSkeleton or "Loading…").

when preset mapping:

when hx-trigger value
viewport (default) revealed
reveal pjx:reveal from:closest [data-pjx-region] once
load load

Load more on scroll (infinite-scroll sentinel). Drop a PJXLazyLoad at the end of a list or <tbody> with when="viewport" and the matching tag. When it scrolls into view it hx-gets url and outerHTML-replaces itself with the server's next batch plus a fresh PJXLazyLoad carrying the next cursor; the loop ends when the server stops emitting a sentinel. The scroll container is just a max-height; overflow:auto wrapper (or the page) — no extra component needed. Ships no JS (htmx's revealed does the visibility detection).

<tbody id="rows">
  <!-- … rendered rows … -->
  <PJXLazyLoad url="/rows?cursor=20" tag="tr" content="Loading…"/>
</tbody>
DOM & classes

Root .pjx-lazy-load (element chosen by tag); no JS (pure HTMX). data-pjx-region on a PJXTabPanel host fires pjx:reveal/pjx:before-reveal events that when="reveal" listens for.

Classes: pjx-lazy-load (unstyled hook). No theming tokens.

Python
PJXLazyLoad(
    id="comments", url="/posts/42/comments", content=PJXSkeleton(id="comments-skel")
)

Form controls

PJXChipInput

Tag-style multi-value input. Each chip carries its own <input type="hidden"> so values post with any enclosing form and removal is pure DOM removal. Assets: pjx_chip_input.css, pjx_chip_input.js.

<PJXChipInput name="tags" values='["python", "jinja2", "htmx"]' placeholder="Add tag…"/>
Props & API
Field Type Default Description
name str name attribute on the hidden inputs and the data-name data attribute.
values list[str] [] Initial chip values.
placeholder str "Add…" Placeholder text in the text field.
remove_label str "Remove" aria-label on every remove button; also stored as data-remove-label so JS can copy it into dynamically built chips.
disabled bool False When True, no text field or remove buttons are rendered; data-disabled is set on the root.

Notes:

  • values are plain-text chip labels and are HTML-escaped on render (output is escaped by default — see Escaping & slots). Passing markup shows it as literal text, not HTML.
  • Duplicate entries are silently dropped (the field clears).
  • Chips added client-side exist only in the DOM until the form posts; an htmx swap of the surrounding region re-renders from server state.
Style tokens
Token Default
--pjx-chip-input-gap 0.375rem
--pjx-chip-input-chip-bg var(--surface-alt)
--pjx-chip-input-chip-fg var(--text)
--pjx-chip-input-chip-radius var(--radius-full)
--pjx-chip-input-border var(--border)
--pjx-chip-input-focus var(--border-focus, var(--border))
--pjx-chip-input-radius var(--radius-md)
--pjx-chip-input-padding 0.375rem 0.5rem
DOM & classes

Root div.pjx-chip-input[data-pjx-chip-input][data-name][data-remove-label]; state: [data-disabled]. Each chip: span.pjx-chip-input__chip[data-pjx-chip] containing a .pjx-chip-input__label, input[type=hidden], and (when enabled) button.pjx-chip-input__remove[data-pjx-chip-remove]. Text field: input.pjx-chip-input__field. Events (bubble from root): pjx:chip-input:before-add (detail {value}), pjx:chip-input:add, pjx:chip-input:before-remove (detail {value}), pjx:chip-input:remove* = cancelable. Commit triggers: Enter, ,, focusout, submit (form submit commits pending text); Backspace on empty field removes the last chip.

Classes: pjx-chip-input, pjx-chip-input__chip, pjx-chip-input__label, pjx-chip-input__remove, pjx-chip-input__field.

Python
PJXChipInput(
    name="tags",
    values=["python", "jinja2", "htmx"],
    placeholder="Add tag…",
)

PJXFormField

Labelled control wrapper with help text and error state. Assets: pjx_form_field.css only.

<PJXFormField label="Email address" for_id="demo-email" help="We'll never share your email with anyone." required="true">
  <input id="demo-email" type="email" name="email" placeholder="you@example.com">
</PJXFormField>
Props & API
Field Type Default Description
label str "" Label text; renders <label> only when non-empty.
for_id str "" When set, adds for="{{ for_id }}" to the label.
content str \| BaseComponent "" Control HTML (an <input>, <select>, etc.).
help str "" Help text shown below the control — suppressed when error is set.
error str "" Error message; adds pjx-form-field--error to the root and a role="alert" paragraph with id {{ id }}-error.
required bool False Renders a <span class="pjx-form-field__required" aria-hidden="true">*</span> inside the label.
Style tokens
Token Default
--pjx-form-field-gap 0.375rem
--pjx-form-field-label-size 0.875em
--pjx-form-field-label-color var(--text)
--pjx-form-field-help-color var(--text-muted)
--pjx-form-field-error #b3261e
DOM & classes

Root div.pjx-form-field; error state div.pjx-form-field--error. Help paragraph id: {{ id }}-help. Error paragraph id: {{ id }}-error, role="alert". No JS.

Classes: pjx-form-field, pjx-form-field--error, pjx-form-field__label, pjx-form-field__required, pjx-form-field__control, pjx-form-field__help, pjx-form-field__error.

Python
PJXFormField(
    label="Email address",
    for_id="demo-email",
    content='<input id="demo-email" type="email" name="email" placeholder="you@example.com">',
    help="We'll never share your email with anyone.",
    required=True,
)

PJXToggleSwitch

Accessible on/off toggle backed by a visually-hidden checkbox. Assets: pjx_toggle_switch.css only — no JS.

<PJXToggleSwitch name="notifications" checked="true" label="Email notifications"/>
Props & API
Field Type Default Description
name str "" name attribute on the hidden checkbox.
value str "on" value attribute on the checkbox.
checked bool False When True, adds checked to the checkbox.
label str "" Visible label text rendered after the track; omitted when empty.
disabled bool False When True, adds disabled to the checkbox.
Style tokens
Token Default
--pjx-toggle-switch-width 2.75rem
--pjx-toggle-switch-height 1.5rem
--pjx-toggle-switch-track var(--border)
--pjx-toggle-switch-track-on var(--brand)
--pjx-toggle-switch-thumb #fff
--pjx-toggle-switch-radius var(--radius-full)
--pjx-toggle-switch-gap 0.5rem
DOM & classes

Root label.pjx-toggle-switch wrapping a visually-hidden input[type=checkbox].pjx-toggle-switch__input (uses clip-path: inset(50%), not display:none — keeps focus and click). CSS keys on :checked + .pjx-toggle-switch__track for the active state and :focus-visible + .pjx-toggle-switch__track for the ring. No JS API.

Classes: pjx-toggle-switch, pjx-toggle-switch__input, pjx-toggle-switch__track, pjx-toggle-switch__thumb, pjx-toggle-switch__label.

Python
PJXToggleSwitch(name="notifications", checked=True, label="Email notifications")

PJXSegmentedControl

Pill-style radio group for mutually exclusive options. Assets: pjx_segmented_control.css only — no JS.

<PJXSegmentedControl name="view" options='[["list", "List"], ["grid", "Grid"], ["table", "Table"]]' selected="grid"/>
Props & API
Field Type Default Description
name str name on all radio inputs.
options list[tuple[str, str]] [] (value, label) pairs. Also accepts a JSON string when passed as a tag attribute.
selected str "" Value of the pre-checked option.
disabled bool False When True, adds disabled to all radio inputs.
Style tokens
Token Default
--pjx-segmented-control-bg var(--surface-alt)
--pjx-segmented-control-border var(--border)
--pjx-segmented-control-radius var(--radius-full)
--pjx-segmented-control-gap 0.25rem
--pjx-segmented-control-padding 0.25rem
--pjx-segmented-control-active-bg var(--surface)
--pjx-segmented-control-active-fg var(--text)
--pjx-segmented-control-active-shadow 0 1px 3px rgb(0 0 0 / 0.12)
--pjx-segmented-control-fg var(--text-muted)
--pjx-segmented-control-focus var(--border-focus, var(--brand))
DOM & classes

Root div.pjx-segmented-control[role="radiogroup"]. Each option: label.pjx-segmented-control__segment > input[type=radio].pjx-segmented-control__input (visually hidden, clip-path: inset(50%)) + span.pjx-segmented-control__text. CSS keys on :checked + .pjx-segmented-control__text for the active segment. No JS API.

Classes: pjx-segmented-control, pjx-segmented-control__segment, pjx-segmented-control__input, pjx-segmented-control__text.

Python
PJXSegmentedControl(
    name="view",
    options=[("list", "List"), ("grid", "Grid"), ("table", "Table")],
    selected="grid",
)

PJXPasswordInput

Password field with a show/hide toggle button. Assets: pjx_password_input.css, pjx_password_input.js.

<PJXPasswordInput name="password" placeholder="Enter your password" autocomplete="current-password" required="true"/>
Props & API
Field Type Default Description
name str "password" name on the <input>.
placeholder str "" placeholder on the <input>; omitted when empty.
autocomplete str "current-password" autocomplete attribute.
required bool False When True, adds required to the <input>.
show_label str "Show password" Static aria-label on the toggle button; visibility state is conveyed by aria-pressed (ARIA toggle-button pattern).
Style tokens
Token Default
--pjx-password-input-border var(--border)
--pjx-password-input-radius var(--radius-md)
--pjx-password-input-focus var(--border-focus, var(--brand))
--pjx-password-input-toggle-color var(--text-muted)
--pjx-password-input-eye-size 1.25rem
DOM & classes

Root div.pjx-password-input[data-pjx-password]. Field: input.pjx-password-input__field with id {{ id }}-field. Toggle button: button.pjx-password-input__toggle[data-pjx-password-toggle]; aria-pressed reflects state ("false" when hidden, "true" when shown); .pjx-password-input__toggle--on class added when visible. No pjx:* events — state is readable from aria-pressed on the button. No JS API under window.pjx.

Classes: pjx-password-input, pjx-password-input__field, pjx-password-input__toggle, pjx-password-input__toggle--on, pjx-password-input__eye.

Python
PJXPasswordInput(
    name="password",
    placeholder="Enter your password",
    autocomplete="current-password",
    required=True,
)

PJXPaginator

Accessible page-navigation bar. Assets: pjx_paginator.css only.

<PJXPaginator page="4" total_pages="12" url="/items?page={page}"/>
Props & API

Pairs naturally with PJXTable: point target at the table body or its wrapper and return the re-paged fragment from your view to update both the rows and the paginator in one swap.

Field Type Default Description
page int (required) Current page number (clamped to [1, total_pages] internally).
total_pages int (required) Total number of pages (must be ≥ 1).
url str (required) URL template; must contain {page} (e.g. /items?page={page}).
target str "" CSS selector for the htmx swap target. When set, every link gets hx-get/hx-target/hx-swap (and hx-push-url when push_url=True); when empty, links render as plain <a href>.
swap str "innerHTML" htmx hx-swap strategy; only used when target is set.
push_url bool False When True and target is set, adds hx-push-url="true" to each link.
siblings int 1 Pages shown on each side of the current page (≥ 0).
boundaries int 1 Pages shown at each end of the range (≥ 0).
prev_next bool True Render Prev / Next controls.
first_last bool False Render First / Last controls.
prev_label str "Prev" Label for the Prev control.
next_label str "Next" Label for the Next control.
first_label str "First" Label for the First control.
last_label str "Last" Label for the Last control.
aria_label str "Pagination" aria-label on the root <nav>.
class_name AttrValue "" Extra CSS class(es) on the root element.
Style tokens
Token Default
--pjx-paginator-gap 0.25rem
--pjx-paginator-link-fg var(--text)
--pjx-paginator-link-bg transparent
--pjx-paginator-link-hover-bg var(--surface-alt)
--pjx-paginator-current-fg var(--brand)
--pjx-paginator-current-bg var(--brand-subtle)
--pjx-paginator-border var(--border)
--pjx-paginator-radius var(--radius-sm)
--pjx-paginator-disabled-opacity 0.4
DOM & classes

Root <nav class="pjx-paginator" aria-label="..."> containing <ul class="pjx-paginator__list"><li class="pjx-paginator__item"> per entry. The current page renders as <span class="pjx-paginator__link pjx-paginator__link--current" aria-current="page">. Ellipsis gaps render as <span class="pjx-paginator__ellipsis" aria-hidden="true">. Disabled controls (Prev on page 1, Next on the last page, etc.) render as <span class="pjx-paginator__control pjx-paginator__control--{variant} pjx-paginator__control--disabled" aria-disabled="true">. Each active page link and enabled control renders as <a href="..."> when target is empty, or as <a hx-get="..." hx-target="..." hx-swap="..."> when target is set. No JS.

Classes: pjx-paginator, pjx-paginator__list, pjx-paginator__item, pjx-paginator__link, pjx-paginator__link--current, pjx-paginator__ellipsis, pjx-paginator__control, pjx-paginator__control--first, pjx-paginator__control--prev, pjx-paginator__control--next, pjx-paginator__control--last, pjx-paginator__control--disabled.

Python
PJXPaginator(page=4, total_pages=12, url="/items?page={page}", target="#list")

```