PrimeFaces Panels, Menus and Dialogs

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against PrimeFaces 16.0.0 (released 2026-09-04) on Jakarta Faces 4.1 (API jakarta.faces-api 4.1.2, Jakarta EE 11), with Eclipse Mojarra 4.1.17 and Apache MyFaces Core 4.1.4, Tomcat 11.0.26 with Weld 6.0.4.Final (Tomcat and Weld; other servers are on the Jakarta EE servers page) and Java 17 or later (the examples were compiled with --release 21). PrimeFaces 16 dropped JavaServer Faces and Java EE (javax.*) support: its baseline is Jakarta Faces 4.0 and newer, so everything below also applies to Faces 4.0. The previous line, PrimeFaces 15.0.x, still has the same components with the differences listed in What Changed per Version. Every example was deployed as an exploded Bookshelf WAR, once on Mojarra and once on MyFaces, and driven in headless Chrome 155 (clicks, Ajax requests counted, results read back from the DOM) on 2026-10-08. A claim that was not run is marked as such. The component and attribute names were checked against the tag library descriptor inside primefaces-16.0.0.jar (176 tags) and against the PrimeFaces 16 documentation, and the Java API against the classes in the jar.

The components of this page are the layout and interaction part of PrimeFaces: containers that group other components (panel, accordion, tab view, toolbar), menus that navigate or run actions, and overlays that interrupt the user (dialogs, confirmations, a blocking mask) or run behind the scenes (poll). The input components, p:ajax, validation, p:growl and p:messages belong to the PrimeFaces Ajax and forms page of this section; the data components (p:dataTable, trees, p:schedule) to the PrimeFaces data components page. They are named here without repeating them, and the examples use a p:dataTable and a p:growl only as scenery. The PrimeFaces setup, themes and icons are on the PrimeFaces getting started page. Plain Faces behaviour that this page relies on is on the Ajax page and the search expressions page.

What These Components Have in Common

Before the individual components, five facts explain almost every surprise in this family. All were observed.

They are Faces components with a JavaScript widget. Each component renders its markup and a script that creates a client object, a widget (PrimeFaces.cw("Panel","detailsPanel",{...}) in the page source). The widget is registered under the component’s widgetVar, which is generated (widget_f_details) unless you set it, and is reached from script with PF('detailsPanel'). That is how PF('editDialog').show() and PF('importPoll').stop() work. A widgetVar is a name in a global table, so give it a name that is unique in the page and does not equal the id of any element: the id of an element becomes a property of window, and a widget variable of the same name is a recipe for confusion (the examples use ids such as details with the widget variable detailsPanel).

The namespace is primefaces. PrimeFaces 16 declares the tag library as xmlns:p="primefaces" (the Faces 4.0 URN style) and keeps the older http://primefaces.org/ui working. Both are used in the wild; this section uses the short one.

Ajax defaults to process="@all". A p:commandButton, a p:menuitem or a p:poll without process posted jakarta.faces.partial.execute=@all in every request below, unlike f:ajax, whose default is @this (see execute and render). Row buttons that only need the row should say process="@this", otherwise every other input of the page is decoded, converted and validated for a click on one row.

update is resolved from the source component’s naming container. A p:accordionPanel with value/var, a p:dataTable, a ui:repeat and a p:tabView are naming containers. An update="msgs" written inside one of them looks for msgs inside it and, on both implementations in the Development project stage, failed the whole page with an error page:

Cannot find component for expressions "msgs" referenced from "f:acc".

The fix is an absolute id from the view root, update=":f:msgs", which is what the examples below do. The rules are the ones of the search expressions page; PrimeFaces components resolve them strictly, so a wrong id is an error on the first render, not a silently ignored update.

Ajax actions need a form. A p:poll or a menu item with an action sends its request with a form’s fields and the view state. With no h:form in the page at all, the requests of a poll and of a p:menuitem were posted every second without jakarta.faces.ViewState, the listener never ran, and the server logged nothing. When another form existed elsewhere in the page, PrimeFaces used it (the request carried that form’s name) and the listener ran. Rely on neither: put actions in a form, and keep a page with menus and polls to one form or to forms that each own their actions.

p:panel

p:panel is the basic grouping box: an optional header and footer, a content area, and three built-in header features, toggling (collapse), closing and an options menu. All three are switched on by attributes and facets:

Attribute or facet Meaning

header, footer and facets header, footer

Texts, or markup in the facets. When an attribute and the facet of the same name are both given, the facet wins (PrimeFaces documentation).

toggleable, toggleSpeed (default 500 ms), toggleOrientation (vertical or horizontal), toggleableHeader

A toggle icon in the title bar, or the whole title bar when toggleableHeader is true.

collapsed

The initial state of a toggleable panel; also where the server state is kept (below).

closable, closeSpeed, visible

A close icon; visible="false" renders the panel hidden.

facet options

A gear icon in the title bar that opens the p:menu placed in the facet.

facet actions

Your own icons in the title bar (the PrimeFaces documentation shows h:commandLink with the ui-panel-titlebar-icon classes).

multiViewState

Keeps the state across views of the same session (default false); not exercised here.

renderEmptyFacets

Renders the facets even if their children are not rendered (default false).

Two Ajax events exist, toggle and close, handled with p:ajax. The listener of toggle receives a org.primefaces.event.ToggleEvent (getVisibility() is Visibility.VISIBLE or HIDDEN), the listener of close a org.primefaces.event.CloseEvent.

panel.xhtml — a toggleable, closable panel with a footer and an options menu
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:p="primefaces">
<ui:composition template="/WEB-INF/templates/layout.xhtml">
  <ui:define name="title">Details panel</ui:define>
  <ui:define name="content">
    <h:form id="f">
      <p:growl id="msgs"/>
      <p:panel id="details" header="Book details" widgetVar="detailsPanel"
               toggleable="true" closable="true" toggleSpeed="200"
               collapsed="#{panelsBean.detailsCollapsed}" visible="#{panelsBean.detailsVisible}">
        <p:ajax event="toggle" listener="#{panelsBean.onToggle}" update="msgs"/>
        <p:ajax event="close" listener="#{panelsBean.onClose}" update="msgs"/>
        <h:outputText value="Dune, Frank Herbert, 1965"/>
        <f:facet name="footer">
          <h:outputText value="Showing 1 of 3 books"/>
        </f:facet>
        <f:facet name="options">
          <p:menu>
            <p:menuitem value="All books" outcome="/accordion.xhtml"/>
          </p:menu>
        </f:facet>
      </p:panel>
    </h:form>
  </ui:define>
</ui:composition>
</html>
PanelsBean.java — remembers the panel and tab state on the server
package org.example.bookshelf.web;

import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.List;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;
import org.primefaces.event.CloseEvent;
import org.primefaces.event.TabChangeEvent;
import org.primefaces.event.TabCloseEvent;
import org.primefaces.event.TabEvent;
import org.primefaces.event.ToggleEvent;
import org.primefaces.model.Visibility;

@Named
@ViewScoped
public class PanelsBean implements Serializable {

    @Inject
    private transient BookService books;

    private boolean detailsVisible = true;
    private boolean detailsCollapsed;
    private int activeTab;

    public List<Book> getBooks() {
        return books.findAll();
    }

    /** Remember the state on the server, so that a re-render shows the panel as the user left it. */
    public void onToggle(ToggleEvent event) {
        detailsCollapsed = event.getVisibility() == Visibility.HIDDEN;
        info("Panel " + (detailsCollapsed ? "collapsed" : "expanded"));
    }

    public void onClose(CloseEvent event) {
        detailsVisible = false;
        info("Panel closed");
    }

    public void onTabChange(TabChangeEvent<?> event) {
        info("Tab: " + event.getTab().getTitle());
    }

    public void onTabClose(TabCloseEvent<?> event) {
        info("Closed tab: " + event.getTab().getTitle());
    }

    /** Called through tabController: tabs may be opened, but not closed. */
    public boolean isTabActionAllowed(TabEvent<?> event) {
        return !(event instanceof TabCloseEvent);
    }

    private void info(String text) {
        FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, text, null));
    }

    public boolean isDetailsVisible() { return detailsVisible; }
    public void setDetailsVisible(boolean detailsVisible) { this.detailsVisible = detailsVisible; }
    public boolean isDetailsCollapsed() { return detailsCollapsed; }
    public void setDetailsCollapsed(boolean detailsCollapsed) { this.detailsCollapsed = detailsCollapsed; }
    public int getActiveTab() { return activeTab; }
    public void setActiveTab(int activeTab) { this.activeTab = activeTab; }
}

What the headless run showed, identical on Mojarra and MyFaces:

  • Clicking the toggle icon sent jakarta.faces.behavior.event=toggle with the hidden field f:details_collapsed=true; the content area was hidden and the listener ran. The state travels in two hidden inputs of the form, details_collapsed and details_visible, which are part of every later post of that form.

  • The toggle listener ran in the Apply Request Values phase (getCurrentPhaseId() printed APPLY_REQUEST_VALUES), the close listener in Invoke Application, like the tab and poll listeners below. A toggle listener therefore runs before the submitted values are converted or validated and must not rely on the model being updated. See the phases.

  • Because collapsed and visible are bound to properties of the view-scoped bean and the listeners store the new values there, a re-render of the whole form (update="@form" from a refresh button, tried on an earlier version of this page) kept the panel closed. Without the bean the server only knows what the hidden fields post; the example stores the state so that a redisplay agrees with it.

  • Closing is only a client hide plus an event. Reloading the page shows the panel again unless the server remembers the choice (here detailsVisible is lost with the view scope, which is the correct lifetime for a view preference; a preference that must survive needs your own storage).

Other grouping containers exist in the 16.0.0 library and are used the same way (their names are tags of the library; they were not exercised for this page): p:fieldset (a bordered group with a legend, optionally toggleable), p:card, p:scrollPanel, p:overlayPanel (a pop-up anchored to a component), p:sidebar, p:splitter, p:panelGrid, p:outputPanel and p:inplace.

p:accordionPanel

p:accordionPanel stacks p:tab children vertically; one tab is open at a time unless multiple="true". p:tab is the same child component as in the tab view: title (or a title facet), disabled, closable and key (a unique key that the accordion’s active accepts instead of an index since 16, see What Changed per Version). The attributes you will actually use:

Attribute Meaning

active

Open tab index or indexes, or keys (since 16; none or -1 collapses all, all opens all). activeIndex is the deprecated predecessor.

multiple

Several tabs open at once.

dynamic, cache

Only the active tab’s content is rendered, the others are loaded by Ajax when first activated; cache="false" loads again each time.

value, var, varStatus

Iterate: one p:tab is repeated for every element, like ui:repeat.

tabController

A method expression boolean method(TabEvent) that the server runs to allow or refuse a tab change or close.

onTabChange, onTabShow, onTabClose

Client-side callbacks; onTabChange runs before a tab is shown.

toggleSpeed, scrollIntoView, multiViewState

Animation speed, scrolling the active tab into view (start, center, end, nearest), state across views.

The Ajax events are tabChange (a TabChangeEvent) and tabClose (a TabCloseEvent); both carry getTab(), the Tab component.

accordion.xhtml — one tab per book, with a server-side veto
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:p="primefaces">
<ui:composition template="/WEB-INF/templates/layout.xhtml">
  <ui:define name="title">Accordion</ui:define>
  <ui:define name="content">
    <h:form id="f">
      <p:growl id="msgs"/>
      <p:accordionPanel id="books" value="#{panelsBean.books}" var="book"
                        multiple="false" tabController="#{panelsBean.isTabActionAllowed}">
        <p:ajax event="tabChange" listener="#{panelsBean.onTabChange}" update=":f:msgs"/>
        <p:tab title="#{book.title}">
          <h:outputText value="#{book.author} (#{book.year})"/>
        </p:tab>
      </p:accordionPanel>
    </h:form>
  </ui:define>
</ui:composition>
</html>

Observed, on both implementations: clicking the second header sent one request (jakarta.faces.behavior.event=tabChange with a posted ..._tabindex field of 1), the listener ran in Invoke Application and the second header became the active one. To test the veto, isTabActionAllowed was changed to return false: the tab stayed closed in the browser, but the tabChange listener still ran, before the controller (LISTENER then CONTROLLER TabChangeEvent in the log). A listener that changes anything must therefore check the same rule itself; the controller only decides what the client is allowed to show. (The PrimeFaces documentation shows the controller as void; the method returns a boolean.) Three more points from the documentation, all consistent with the run:

  • A single tab cannot be updated by Ajax. Update the whole accordion, or a container inside the tab (the documentation warns about this for the tab view as well).

  • A tabChange listener must not update the accordion or a parent of it. The component is then broken after the re-render (documented, not reproduced here); update the messages component or an element inside a tab instead, as in the example.

  • active="none" renders everything collapsed and lets the renderer skip evaluating tab keys.

p:tabView

p:tabView is the horizontal sibling: tab headers in a row, one panel visible. It shares p:tab, value/var, dynamic/cache, tabindex, multiViewState and the callbacks with the accordion, and adds:

Attribute Meaning

activeIndex

The open tab, a value expression is written when the tab changes (below).

orientation

top (default), left, right, bottom.

effect, effectDuration

A jQuery UI effect and its speed (slow, normal, fast).

scrollable

Keeps the headers on one line with navigation buttons instead of wrapping them.

focusOnError, focusOnLastActiveTab

After a validation failure, activate the tab that contains the first error; remember the last active tab.

touchable

Touch support (global default or primefaces.TOUCHABLE).

facet actions

Extra icons next to the tab headers.

A p:tab with closable="true" shows a close icon and fires tabClose.

tabs.xhtml — lazy tabs with a closable tab
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:p="primefaces">
<ui:composition template="/WEB-INF/templates/layout.xhtml">
  <ui:define name="title">Tabs</ui:define>
  <ui:define name="content">
    <h:form id="f">
      <p:growl id="msgs"/>
      <p:tabView id="tabs" dynamic="true" cache="true" activeIndex="#{panelsBean.activeTab}">
        <p:ajax event="tabChange" listener="#{panelsBean.onTabChange}" update=":f:msgs"/>
        <p:ajax event="tabClose" listener="#{panelsBean.onTabClose}" update=":f:msgs"/>
        <p:tab title="Details">
          <h:outputText value="Dune, Frank Herbert, 1965"/>
        </p:tab>
        <p:tab title="Reviews">
          <h:outputText value="#{panelsBean.books.size()} books on the shelf"/>
        </p:tab>
        <p:tab title="Covers" closable="true">
          <h:outputText value="No covers yet"/>
        </p:tab>
      </p:tabView>
    </h:form>
  </ui:define>
</ui:composition>
</html>

Observed on both implementations: with dynamic="true" the content of the second tab was not in the initial page; clicking the header sent a tabChange request with f:tabs_contentLoad=true and the tab view appeared in the render list (jakarta.faces.partial.render=f:msgs f:tabs), after which the text was there. Going back to the first tab and to the second again sent two more tabChange events without contentLoad, because the contents are cached (cache defaults to true). The listener saw activeTab=1 in the bean, so activeIndex is written to the bound property before the listener runs. Clicking the close icon of Covers removed the header and fired tabClose (Closed tab: Covers); like a closed panel, the tab is back when the page is requested again, so keep the decision on the server (a rendered condition on a bean flag) when it matters. A dynamic tab is a good place for something expensive, and the tab view is the right fit for a book page with Details, Reviews and Covers. An expensive tab belongs to a view-scoped bean; the view scope explains how much a view keeps.

p:toolbar

p:toolbar is a bar for commands: two placeholders, left and right, defined either with facets of those names or with p:toolbarGroup children (align="left" is the default, or right). Any content may go in a group.

toolbar.xhtml — command buttons on the left, an export menu on the right
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:p="primefaces">
<ui:composition template="/WEB-INF/templates/layout.xhtml">
  <ui:define name="title">Toolbar</ui:define>
  <ui:define name="content">
    <h:form id="f">
      <p:growl id="msgs"/>
      <p:toolbar id="tools">
        <p:toolbarGroup align="left">
          <p:button value="New book" icon="pi pi-plus" outcome="/dialogs.xhtml"/>
          <p:commandButton value="Refresh" icon="pi pi-refresh" process="@this" update="@form"/>
        </p:toolbarGroup>
        <p:toolbarGroup align="right">
          <p:menuButton value="Export">
            <p:menuitem value="CSV" url="#"/>
            <p:menuitem value="PDF" url="#"/>
          </p:menuButton>
        </p:toolbarGroup>
      </p:toolbar>
    </h:form>
  </ui:define>
</ui:composition>
</html>

The groups were rendered as ui-toolbar-group-left and ui-toolbar-group-right and the text read New book | Refresh | Export. The PrimeFaces documentation sample still shows p:commandButton type="push" and an image attribute from the ThemeRoller era; the current attribute for an icon is icon (a PrimeIcons class such as pi pi-plus; p:button with outcome is the navigation counterpart). Since 15 a toolbar without any rendered child is not rendered at all, and separators do not count as children. p:menuButton and p:splitButton are the compact ways to put several actions into one button.

All PrimeFaces menus are built from the same pieces. p:menuitem is one entry, p:submenu groups entries (and, in a menubar, nests), p:divider separates them. The menu components differ in how they present and where they sit.

A p:menuitem has three kinds of target, and which one applies is decided by the attribute you set. The PrimeFaces documentation says that url is the key: if it is set, the item is a plain link, so do not set it for an item that runs a command.

Attributes Behaviour

action (and actionListener), update, process

An Ajax request (the default), like a p:commandButton. ajax="false" makes it a normal form POST (the example’s Rebuild index item). immediate, oncomplete, onstart, partialSubmit, resetValues work as on the command components.

outcome, includeViewParams, fragment

A GET navigation resolved with the navigation handler, like h:link (see navigation); the item rendered a plain link to the target view. Safe for bookmarkable targets.

url or href, target

A plain link to any address; target="_blank" opened it in a new tab. The links of the example were https://primefaces.github.io/primefaces/16_0_0/ with _blank and /bookshelf/accordion.xhtml.

icon, iconPos, badge, value, title, ariaLabel

Presentation. escape (default true) controls whether the label is HTML-escaped; see Security Notes for This Family. The tag declares value for the label; several samples in the PrimeFaces documentation write label, which is not an attribute of the 16.0.0 tag.

disabled, rendered

Hide or grey the item (never an authorization, Security Notes for This Family).

The item is also what the p:confirm behaviour, p:confirmDialog, p:confirmPopup and p:confirm, attaches to, and the item you give to a steps or breadcrumb component.

A horizontal navigation bar: p:submenu children that open drop-downs, plus p:menuitem children as root items. Submenus nest for deeper levels. The facets start and end take a logo or a button. autoDisplay (default true) opens the first level on hover, toggleEvent (hover or click), showDelay and hideDelay tune the timing.

menubar.xhtml — submenu, actions with and without Ajax, links and facets
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:p="primefaces">
<ui:composition template="/WEB-INF/templates/layout.xhtml">
  <ui:define name="title">Menubar</ui:define>
  <ui:define name="content">
    <h:form id="f">
      <p:growl id="msgs"/>
      <p:menubar id="nav">
        <f:facet name="start">
          <h:outputText value="Bookshelf"/>
        </f:facet>
        <p:submenu label="Catalogue" icon="pi pi-book">
          <p:menuitem value="All books" outcome="/accordion.xhtml" icon="pi pi-list"/>
          <p:menuitem value="Add book" action="#{siteMenuBean.addBook}" update="msgs" icon="pi pi-plus"/>
          <p:divider/>
          <p:menuitem value="Rebuild index" action="#{siteMenuBean.reindex}" ajax="false"/>
        </p:submenu>
        <p:menuitem value="Docs" url="https://primefaces.github.io/primefaces/16_0_0/" target="_blank"/>
        <f:facet name="end">
          <p:commandButton value="Log out" action="#{siteMenuBean.logout}" update="msgs"/>
        </f:facet>
      </p:menubar>
    </h:form>
  </ui:define>
</ui:composition>
</html>

Observed: the text read Bookshelf | Catalogue | Docs | Log out; the links of the rendered items were # for the items with actions, the view address for the outcome item and the absolute address with target=_blank for the URL item; one divider was rendered; clicking Add book posted an Ajax request with execute=@all and the growl showed Add book requested. There is no p:separator any more: it was deprecated in 15 and removed in 16, use p:divider (What Changed per Version).

p:megaMenu

A menubar whose first-level items open a grid. The root p:submenu contains p:column children and each column holds submenus and menu items (or any markup). orientation is horizontal (default) or vertical.

mega-menu.xhtml — two columns under one root item
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:p="primefaces">
<ui:composition template="/WEB-INF/templates/layout.xhtml">
  <ui:define name="title">Mega menu</ui:define>
  <ui:define name="content">
    <h:form id="f">
      <p:megaMenu id="genres">
        <p:submenu label="Genres" icon="pi pi-tags">
          <p:column>
            <p:submenu label="Fiction">
              <p:menuitem value="Science fiction" outcome="/accordion.xhtml"/>
              <p:menuitem value="Fantasy" outcome="/accordion.xhtml"/>
            </p:submenu>
          </p:column>
          <p:column>
            <p:submenu label="Non-fiction">
              <p:menuitem value="History" outcome="/accordion.xhtml"/>
              <p:menuitem value="Science" outcome="/accordion.xhtml"/>
            </p:submenu>
          </p:column>
        </p:submenu>
      </p:megaMenu>
    </h:form>
  </ui:define>
</ui:composition>
</html>

The mega menu rendered the root item Genres and, below it, the nested lists of the two columns. The hover behaviour was not exercised in the headless run beyond rendering.

p:contextMenu

A menu shown on the right mouse button. for names the component to attach to (a data table, a tree or any component or element); without for it attaches to the whole page. event changes the trigger from contextmenu, targetFilter is a CSS selector to attach to children, beforeShow is a client callback that may cancel, and selectionMode and nodeType belong to the tree and table integrations (see the PrimeFaces data components page).

context-menu.xhtml — a menu on the shelf element
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:p="primefaces">
<ui:composition template="/WEB-INF/templates/layout.xhtml">
  <ui:define name="title">Context menu</ui:define>
  <ui:define name="content">
    <h:form id="f">
      <p:growl id="msgs"/>
      <h:panelGroup id="shelf" layout="block" styleClass="shelf">Right-click the shelf</h:panelGroup>
      <p:contextMenu id="shelfMenu" for="shelf">
        <p:menuitem value="Add book" action="#{siteMenuBean.addBook}" update="msgs" icon="pi pi-plus"/>
        <p:menuitem value="Open catalogue" outcome="/accordion.xhtml"/>
      </p:contextMenu>
    </h:form>
  </ui:define>
</ui:composition>
</html>

A synthetic contextmenu event on the shelf element showed the menu (Add book | Open catalogue) and clicking Add book ran the action by Ajax. Real right-clicks and touch long-presses (touchable) were not exercised. The overlay is appended to the body by default (appendTo="@(body)"), so it is outside the form in the page: keep its items' update to ids that resolve from the item.

A trail of the current position. Define the items declaratively (p:menuitem children) or bind a model, which is the right choice for a trail that depends on the data (the Bookshelf trail is built from the book’s genre). The facet options is aligned to the right. homeDisplay (icon by default, or text), homeIcon, lastItemDisabled and seo (an SEO-oriented markup structure) are the attributes.

breadcrumb.xhtml — a trail from a MenuModel
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:p="primefaces">
<ui:composition template="/WEB-INF/templates/layout.xhtml">
  <ui:define name="title">Breadcrumb</ui:define>
  <ui:define name="content">
    <h:form id="f">
      <p:breadCrumb id="trail" model="#{siteMenuBean.trail}"/>
    </h:form>
  </ui:define>
</ui:composition>
</html>

Rendered as a nav with aria-label="Breadcrumb" around an ordered list (accessible markup since 10); the first item was the home icon (ui-breadcrumb-home-icon) and the text read Science fiction | Dune. An item with an action instead of an outcome needs the trail to be inside a form, as for every Ajax item.

p:steps

An indicator of progress through a workflow. activeIndex is the highlighted item (default 0); readonly (default true) makes the steps not clickable; activeStepExecutable lets the active item keep its action. The steps do not move anything: they show a position that the application decides, which makes them a good companion of a Faces Flow. The Bookshelf borrow request of the Faces Flows page has four views (borrow, period, reason, confirm); one bean turns the current view id into the step index, so the same component serves every view of the flow:

BorrowStepsBean.java — the view of the wizard decides the step
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Named;
import java.util.List;

/** Maps the current view of the borrow wizard to the index of the p:steps item that must be highlighted. */
@Named
@RequestScoped
public class BorrowStepsBean {

    private static final List<String> VIEWS = List.of("borrow", "period", "reason", "confirm");

    public int getActiveIndex() {
        String viewId = FacesContext.getCurrentInstance().getViewRoot().getViewId();
        String name = viewId.substring(viewId.lastIndexOf('/') + 1).replace(".xhtml", "");
        int index = VIEWS.indexOf(name);
        return Math.max(index, 0);
    }
}
borrow/period.xhtml — the second step of the wizard is highlighted
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:p="primefaces">
<ui:composition template="/WEB-INF/templates/layout.xhtml">
  <ui:define name="title">Loan period</ui:define>
  <ui:define name="content">
    <p:steps id="progress" activeIndex="#{borrowStepsBean.activeIndex}">
      <p:menuitem value="Reader"/>
      <p:menuitem value="Loan period"/>
      <p:menuitem value="Reason"/>
      <p:menuitem value="Confirm"/>
    </p:steps>
  </ui:define>
</ui:composition>
</html>

For /borrow/period.xhtml the items rendered as 1 Reader:default disabled, 2 Loan period:highlight, 3 Reason:default disabled and 4 Confirm:default disabled; the confirm view highlighted the fourth. Making the items clickable (readonly="false") lets a reader jump to an earlier step, and the target of that jump is a request like any other: the flow, not the steps component, must decide whether the jump is legal. The flow handles back navigation and the conditions between steps in its own definition; p:steps only draws. (p:wizard is a different component, a form wizard with its own navigation and FlowEvent; it was not exercised for this page.)

Every menu with a model attribute (p:menu, p:menubar, p:megaMenu, p:contextMenu, p:breadCrumb, p:steps, p:tieredMenu and others) can be built in Java. org.primefaces.model.menu.MenuModel holds elements, DefaultMenuModel is the implementation, and DefaultMenuItem, DefaultSubMenu, DefaultSeparator and DefaultMenuColumn are the counterparts of the tags, each with a builder. A command is a method expression in a string; use setParam(key, value) for parameters.

SiteMenuBean.java — a menu and a trail built in Java
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Named;
import org.primefaces.model.menu.DefaultMenuItem;
import org.primefaces.model.menu.DefaultMenuModel;
import org.primefaces.model.menu.DefaultSeparator;
import org.primefaces.model.menu.DefaultSubMenu;
import org.primefaces.model.menu.MenuModel;

@Named
@RequestScoped
public class SiteMenuBean {

    private final MenuModel model = new DefaultMenuModel();
    private final MenuModel trail = new DefaultMenuModel();

    public SiteMenuBean() {
        DefaultSubMenu catalogue = DefaultSubMenu.builder().label("Catalogue").icon("pi pi-book").build();
        catalogue.getElements().add(DefaultMenuItem.builder()
                .value("All books").icon("pi pi-list").outcome("/accordion.xhtml").build());
        catalogue.getElements().add(DefaultMenuItem.builder()
                .value("Add book").icon("pi pi-plus").command("#{siteMenuBean.addBook}").update("msgs").build());
        catalogue.getElements().add(DefaultSeparator.builder().build());
        catalogue.getElements().add(DefaultMenuItem.builder()
                .value("Rebuild index").icon("pi pi-refresh").command("#{siteMenuBean.reindex}").ajax(false).build());
        model.getElements().add(catalogue);
        model.getElements().add(DefaultMenuItem.builder()
                .value("Docs").url("https://primefaces.github.io/primefaces/16_0_0/").target("_blank").build());

        trail.getElements().add(DefaultMenuItem.builder().value("Catalogue").outcome("/accordion.xhtml").build());
        trail.getElements().add(DefaultMenuItem.builder().value("Science fiction").outcome("/mega-menu.xhtml").build());
        trail.getElements().add(DefaultMenuItem.builder().value("Dune").build());
    }

    public void addBook() {
        info("Add book requested");
    }

    public String reindex() {
        info("Index rebuilt");
        return null;
    }

    public void logout() {
        info("Logged out");
    }

    private void info(String text) {
        FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, text, null));
    }

    public MenuModel getModel() { return model; }
    public MenuModel getTrail() { return trail; }
}
menu-model.xhtml — the menubar of the first example, from the model
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:p="primefaces">
<ui:composition template="/WEB-INF/templates/layout.xhtml">
  <ui:define name="title">Menu from a model</ui:define>
  <ui:define name="content">
    <h:form id="f">
      <p:growl id="msgs"/>
      <p:menubar id="nav" model="#{siteMenuBean.model}"/>
    </h:form>
  </ui:define>
</ui:composition>
</html>

The model menubar rendered the same entries as the declarative one (Catalogue | Docs, one separator). A request-scoped bean is the right scope: the model is rebuilt on every request, so it can depend on the current user. It also means that building a model is the place for a per-user decision: omit the items a user may not use, and enforce the rule on the called method, as in Security Notes for This Family. Since 8 the elements are added with getElements().add(...) (addElement is deprecated, DynamicMenuModel was removed).

p:menu is the plain vertical menu. With overlay="true", trigger (the id of the component that opens it) and the positions my and at it becomes a pop-up positioned against the trigger; toggleable="true" lets submenu headers collapse (expanded of the submenu is honoured since 13), and collision and maxHeight keep a tall overlay inside the viewport.

button-menu.xhtml — an overlay menu opened by a button
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:p="primefaces">
<ui:composition template="/WEB-INF/templates/layout.xhtml">
  <ui:define name="title">Overlay menu</ui:define>
  <ui:define name="content">
    <h:form id="f">
      <p:growl id="msgs"/>
      <p:commandButton id="actions" type="button" value="Actions" icon="pi pi-bars"/>
      <p:menu id="actionsMenu" overlay="true" trigger="actions" my="left top" at="left bottom">
        <p:submenu label="Book">
          <p:menuitem value="Add" action="#{siteMenuBean.addBook}" update="msgs"/>
        </p:submenu>
      </p:menu>
    </h:form>
  </ui:define>
</ui:composition>
</html>

Clicking the button (a type="button" so it does not submit) opened the overlay. The other menus are named here and not exercised (their names are tags of the 16.0.0 library): p:tieredMenu (a vertical menu with fly-out submenus), p:slideMenu (drill-down), p:panelMenu (accordion-like tree), p:tabMenu (tab strip that navigates), p:menuButton and p:splitButton (a button with a menu), p:dock and p:speedDial. The PrimeFaces documentation has a page for each.

p:dialog

p:dialog is an overlay window that is not visible until you show it. The two ways to show it are the client API and a server-side script; the examples use the first:

  • PF('editDialog').show() and hide() from onclick, oncomplete or your own script. The widget also has isVisible() and resetPosition().

  • visible="true" renders it open, and PrimeFaces.current().executeScript("...") runs a script after an Ajax response (the method exists in the 16.0.0 API; not exercised here).

Attribute Meaning

header, footer, showHeader; facets header, footer, actions

Title and footer, and your own icons next to the close icon.

modal, dismissibleMask, blockScroll, closeOnEscape, closable

A mask behind the dialog, closing by clicking the mask, preventing page scrolling, closing with Escape, the close icon.

draggable, resizable (both default true), minimizable, maximizable, width, height, minWidth, minHeight

Window behaviour and size.

position, my, positionType (fixed or absolute)

center by default; top, left, a pair like left,top or pixel coordinates.

showEffect, hideEffect

jQuery UI effects (blind, fade, explode, …​).

responsive (default true), fitViewport

Adapt to the screen width; fit within the viewport.

dynamic, cache

Load the content on first show by Ajax (loadContent event); keep it afterwards (default true).

appendTo

A search expression for the element the dialog is moved to in the browser’s DOM, @(body) for example.

focus, onShow, onHide, widgetVar, visible

Focus target, client callbacks, the widget name.

The Ajax events are close (a CloseEvent), open, move, minimize, maximize, restoreMinimize, restoreMaximize, loadContent, resizeStart and resizeStop. The standard use is an edit form:

dialogs.xhtml — a table whose rows open one shared edit dialog
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:p="primefaces">
<ui:composition template="/WEB-INF/templates/layout.xhtml">
  <ui:define name="title">Edit dialog</ui:define>
  <ui:define name="content">
    <h:form id="list">
      <p:growl id="msgs"/>
      <p:dataTable id="books" var="book" value="#{bookDialogBean.books}">
        <p:column headerText="Title">
          <h:outputText value="#{book.title}"/>
        </p:column>
        <p:column headerText="Actions">
          <p:commandButton id="edit" value="Edit" icon="pi pi-pencil" process="@this" update=":editForm"
                           action="#{bookDialogBean.edit(book.id)}" oncomplete="PF('editDialog').show()"/>
        </p:column>
      </p:dataTable>
    </h:form>

    <p:dialog id="editBookDialog" header="Edit book" widgetVar="editDialog" modal="true"
              appendTo="@(body)" resizable="false" draggable="false" closeOnEscape="true">
      <h:form id="editForm">
        <h:panelGrid columns="2">
          <p:outputLabel for="title" value="Title"/>
          <p:inputText id="title" value="#{bookDialogBean.title}" required="true"/>
          <p:outputLabel for="author" value="Author"/>
          <p:inputText id="author" value="#{bookDialogBean.author}" required="true"/>
        </h:panelGrid>
        <p:messages id="editMessages"/>
        <p:commandButton value="Save" action="#{bookDialogBean.save}"
                         update="editMessages :list:books :list:msgs"
                         oncomplete="if (args &amp;&amp; args.saved) PF('editDialog').hide()"/>
        <p:commandButton value="Cancel" type="button" onclick="PF('editDialog').hide()"/>
      </h:form>
    </p:dialog>
  </ui:define>
</ui:composition>
</html>
BookDialogBean.java — load on the server, save, tell the client it worked
package org.example.bookshelf.web;

import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.List;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;
import org.primefaces.PrimeFaces;

@Named
@ViewScoped
public class BookDialogBean implements Serializable {

    @Inject
    private transient BookService books;

    private long id;
    private String title;
    private String author;
    private int year;

    public List<Book> getBooks() {
        return books.findAll();
    }

    /** Called from a row button: reads the book on the server, never trusting client-sent values. */
    public void edit(long bookId) {
        Book book = books.findById(bookId).orElseThrow();
        this.id = book.id();
        this.title = book.title();
        this.author = book.author();
        this.year = book.year();
    }

    public void save() {
        books.save(new Book(id, title, author, year));
        info("Saved " + title);
        PrimeFaces.current().ajax().addCallbackParam("saved", true);
    }

    public void delete(long bookId) {
        Book book = books.findById(bookId).orElseThrow();
        books.delete(bookId);
        info("Deleted " + book.title());
    }

    public void deleteAll() {
        books.deleteAll();
        info("Deleted every book");
    }

    private void info(String text) {
        FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, text, null));
    }

    public long getId() { return id; }
    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public String getAuthor() { return author; }
    public void setAuthor(String author) { this.author = author; }
    public int getYear() { return year; }
    public void setYear(int year) { this.year = year; }
}

How it fits together, as run:

  1. The row button runs edit(book.id) with process="@this", copies the book’s fields from the server-side service into the view-scoped bean (it does not use a field posted by the browser) and updates :editForm so the inputs show the book; oncomplete then shows the dialog. The id of the dialog’s form is absolute because the dialog is outside the first form.

  2. Save submits editForm. With an empty title the request ran Process Validations, the message Title: Validation Error: Value is required. appeared in the dialog’s p:messages, the dialog stayed open and save did not run.

  3. With a valid title save() ran and PrimeFaces.current().ajax().addCallbackParam("saved", true) put a value in the response. The oncomplete script read it from the args object (args.saved) and hid the dialog; the table (:list:books) and the growl were updated in the same response, and the first-row text read the new title. args is how a server decision reaches script, and it avoids closing a dialog whose validation failed (the &amp;&amp; in the file is the XML escape of &&).

The dialog was a child of the body (parentElement.tagName == BODY) because of appendTo="@(body)". That is deliberate and has a consequence: the dialog’s content is no longer inside the form that declared it in the page source, so give it its own h:form, as above. The PrimeFaces documentation names two reasons for appendTo: nested dialogs and a dialog inside a clipping container (a tab view, an accordion). It also says not to place a dialog in a table cell or in a positioned or overflow: hidden container. dynamic="true" was run on a separate small page: the dialog’s text was absent from the initial HTML; PF(...).show() sent a loadContent request (..._contentLoad=true), after which the text was present, and the loadContent listener ran.

PrimeFaces 16 changed the markup of the dialog (the outer ui-dialog is now the scrollable layer and a new ui-dialog-box is the box); custom CSS that targets the old structure needs to be checked, and for a dialog with long content set either modal="true" with blockScroll="true", or positionType="absolute", or use fitViewport="true" (migration guide).

p:confirmDialog, p:confirmPopup and p:confirm

A confirmation replaces the browser’s window.confirm with a styled dialog (p:confirmDialog) or a small overlay anchored to the button (p:confirmPopup). Each has two modes.

Local mode: a p:confirmDialog with a widgetVar, header, message (or a message facet) and severity (alert, the default, or info), whose buttons you write, shown by PF('deleteAllDialog').show(). It is a dialog with a message, and the p:dialog advice applies.

Global mode: one p:confirmDialog global="true" (or p:confirmPopup global="true") per page, and a p:confirm child behaviour in each button that needs it. The behaviour holds the header, message, icon and, for the popup, type="popup"; its attributes yesButtonLabel, noButtonLabel and the icon and class variants (yesButtonIcon, noButtonClass) override the dialog’s buttons; disabled switches it off and escape (default true) controls HTML escaping of the message. The buttons that answer the question carry style classes: ui-confirmdialog-yes and ui-confirmdialog-no for the dialog, ui-confirm-popup-yes and ui-confirm-popup-no for the popup. p:confirm can be put in a p:commandButton, p:commandLink or p:menuitem.

confirm.xhtml — global confirmations for the row buttons and a local dialog for Delete all
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:p="primefaces">
<ui:composition template="/WEB-INF/templates/layout.xhtml">
  <ui:define name="title">Confirmations</ui:define>
  <ui:define name="content">
    <h:form id="list">
      <p:growl id="msgs"/>
      <p:dataTable id="books" var="book" value="#{bookDialogBean.books}">
        <p:column headerText="Title">
          <h:outputText value="#{book.title}"/>
        </p:column>
        <p:column headerText="Actions">
          <p:commandButton id="delete" value="Delete" icon="pi pi-trash" process="@this" update=":list:books :list:msgs"
                           action="#{bookDialogBean.delete(book.id)}">
            <p:confirm header="Delete book" message="Delete #{book.title}?" icon="pi pi-exclamation-triangle"/>
          </p:commandButton>
          <p:commandButton id="remove" value="Remove" process="@this" update=":list:books :list:msgs"
                           action="#{bookDialogBean.delete(book.id)}">
            <p:confirm type="popup" message="Remove #{book.title} from the shelf?"/>
          </p:commandButton>
        </p:column>
      </p:dataTable>

      <p:confirmDialog global="true" showEffect="fade" hideEffect="fade">
        <p:commandButton value="Yes" type="button" styleClass="ui-confirmdialog-yes" icon="pi pi-check"/>
        <p:commandButton value="No" type="button" styleClass="ui-confirmdialog-no" icon="pi pi-times"/>
      </p:confirmDialog>
      <p:confirmPopup global="true">
        <p:commandButton value="No" type="button" styleClass="ui-confirm-popup-no"/>
        <p:commandButton value="Yes" type="button" styleClass="ui-confirm-popup-yes"/>
      </p:confirmPopup>

      <p:commandButton id="deleteAll" value="Delete all" type="button" onclick="PF('deleteAllDialog').show()"/>
      <p:confirmDialog widgetVar="deleteAllDialog" header="Delete all" severity="alert"
                       message="Delete every book on the shelf?">
        <p:commandButton value="Delete" action="#{bookDialogBean.deleteAll}" update=":list:books :list:msgs"
                         oncomplete="PF('deleteAllDialog').hide()"/>
        <p:commandButton value="Keep" type="button" onclick="PF('deleteAllDialog').hide()"/>
      </p:confirmDialog>
    </h:form>
  </ui:define>
</ui:composition>
</html>

Observed on both implementations:

  • The delete button opened the dialog with the message Delete Dune? and the title Delete book. Answering No sent no request: the table still had three rows. Answering Yes ran the button’s original action and the response removed the row (Deleted Dune).

  • The popup variant rendered Remove Neuromancer from the shelf? | No | Yes anchored to its button.

  • The local dialog opened with its own two buttons; Delete ran the Ajax action, Keep only hid the dialog.

  • Each global component is meant to be a singleton per page (documentation): one dialog and one popup serve every p:confirm.

The pattern is client-side confirmation: the question is asked in the browser, and an attacker, a script or a test can send the request that the buttons would send. Treat the dialog as a courtesy, and authorize and validate the action on the server as in Security Notes for This Family; the client behaviours page shows the same point for a plain onclick confirmation. p:confirm is a behavior (org.primefaces.behavior.confirm.ConfirmBehavior), which is why it is a child of the component it decorates.

The Dialog Framework

The dialogs above are declared in the page. The dialog framework is the programmatic alternative: the server decides at run time to open a view in a dialog. The page of the dialog is an ordinary Faces view that is loaded into an iframe inside a dialog that PrimeFaces creates; the dialog is destroyed when the view closes it. It is the right tool for a reusable picker or editor that has its own page, its own bean and its own validation. The entry point is PrimeFaces.current().dialog() (since 7 the replacement of the RequestContext of older code; see What Changed per Version), whose methods in 16.0.0 are:

Method Purpose

openDynamic(String outcome)

Open the view that the navigation outcome names, with defaults.

openDynamic(String outcome, DialogFrameworkOptions options, Map<String, List<String>> params)

With options (a builder) and request parameters for the dialog’s view. An overload takes a Map<String, Object> of options instead.

closeDynamic(Object data)

Called by the view in the dialog: close it and pass data (an object, or null) back.

showMessageDynamic(FacesMessage), showMessageDynamic(FacesMessage, boolean)

Show a message in a small dialog without a view of your own.

The PrimeFaces 16 documentation calls the last method showMessageInDialog; the 16.0.0 jar has showMessageDynamic and no showMessageInDialog (verified with javap). The documentation sample is out of date; use showMessageDynamic.

Registration

The framework needs three handlers in faces-config.xml. PrimeFaces 16 does not register them itself (its own faces-config.xml registers only the DialogKeepFlashPhaseListener); the documentation says so. Without them nothing happened when the button was pressed: the request was answered, no dialog appeared, and the log was silent. With them it worked on Mojarra and on MyFaces.

WEB-INF/faces-config.xml — the dialog framework handlers
<?xml version="1.0" encoding="UTF-8"?>
<faces-config xmlns="https://jakarta.ee/xml/ns/jakartaee"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-facesconfig_4_1.xsd"
              version="4.1">
  <application>
    <action-listener>org.primefaces.application.DialogActionListener</action-listener>
    <navigation-handler>org.primefaces.application.DialogNavigationHandler</navigation-handler>
    <view-handler>org.primefaces.application.DialogViewHandler</view-handler>
  </application>
</faces-config>

The three classes plug into the action listener, navigation handler and view handler extension points of Faces. If another library or your own code registers one of those as well, the chain rules of the extending Faces page apply; no combination was exercised here.

A Launcher and a Dialog View

The host page has buttons that call a bean; the bean opens df-edit with the id as a request parameter; the dialog view is an ordinary page that reads the id with a view parameter (see view parameters and view actions) and closes itself with the saved book.

df-host.xhtml — the host page
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:p="primefaces">
<ui:composition template="/WEB-INF/templates/layout.xhtml">
  <ui:define name="title">Dialog framework</ui:define>
  <ui:define name="content">
    <h:form id="f">
      <p:growl id="msgs"/>
      <h:outputText id="last" value="Last edited: #{dialogLauncherBean.lastEdited}"/>
      <ui:repeat value="#{dialogLauncherBean.books}" var="book">
        <p:commandButton value="Edit #{book.title}" process="@this" action="#{dialogLauncherBean.edit(book.id)}">
          <p:ajax event="dialogReturn" listener="#{dialogLauncherBean.onReturn}" update=":f:msgs :f:last"/>
        </p:commandButton>
      </ui:repeat>
      <p:commandButton id="notice" value="Rebuild index" process="@this" action="#{dialogLauncherBean.notice}"/>
    </h:form>
  </ui:define>
</ui:composition>
</html>
DialogLauncherBean.java — opens the dialog and receives the result
package org.example.bookshelf.web;

import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;
import org.primefaces.PrimeFaces;
import org.primefaces.event.SelectEvent;
import org.primefaces.model.DialogFrameworkOptions;

@Named
@ViewScoped
public class DialogLauncherBean implements Serializable {

    @Inject
    private transient BookService books;

    private String lastEdited = "none";

    public List<Book> getBooks() {
        return books.findAll();
    }

    public void edit(long id) {
        DialogFrameworkOptions options = DialogFrameworkOptions.builder()
                .modal(true)
                .draggable(false)
                .resizable(false)
                .contentWidth("520")
                .contentHeight("320")
                .build();
        PrimeFaces.current().dialog().openDynamic("df-edit", options, Map.of("id", List.of(String.valueOf(id))));
    }

    public void onReturn(SelectEvent<Book> event) {
        Book book = event.getObject();
        lastEdited = book == null ? "cancelled" : book.title();
        FacesContext.getCurrentInstance().addMessage(null,
                new FacesMessage(FacesMessage.SEVERITY_INFO, "Dialog returned: " + lastEdited, null));
    }

    public void notice() {
        PrimeFaces.current().dialog().showMessageDynamic(
                new FacesMessage(FacesMessage.SEVERITY_INFO, "Index rebuilt", "It took 0 ms."));
    }

    public String getLastEdited() { return lastEdited; }
}
df-edit.xhtml — the view that is shown inside the dialog
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:p="primefaces">
<f:metadata>
  <f:viewParam name="id" value="#{bookEditorBean.id}"/>
  <f:viewAction action="#{bookEditorBean.load}"/>
</f:metadata>
<h:head>
  <title>Edit book</title>
</h:head>
<h:body>
  <h:form id="editor" rendered="#{bookEditorBean.found}">
    <p:messages/>
    <h:panelGrid columns="2">
      <p:outputLabel for="title" value="Title"/>
      <p:inputText id="title" value="#{bookEditorBean.title}" required="true"/>
      <p:outputLabel for="author" value="Author"/>
      <p:inputText id="author" value="#{bookEditorBean.author}" required="true"/>
    </h:panelGrid>
    <p:commandButton id="save" value="Save" action="#{bookEditorBean.save}"/>
    <p:commandButton id="cancel" value="Cancel" action="#{bookEditorBean.cancel}" immediate="true"/>
  </h:form>
  <h:outputText rendered="#{not bookEditorBean.found}" value="No such book"/>
</h:body>
</html>
BookEditorBean.java — the dialog view’s own bean
package org.example.bookshelf.web;

import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;
import org.primefaces.PrimeFaces;

/** Backs the view that is shown inside the dialog (df-edit.xhtml). */
@Named
@ViewScoped
public class BookEditorBean implements Serializable {

    @Inject
    private transient BookService books;

    private long id;
    private String title;
    private String author;
    private int year;
    private boolean found;

    public void load() {
        books.findById(id).ifPresent(b -> {
            this.title = b.title();
            this.author = b.author();
            this.year = b.year();
            this.found = true;
        });
    }

    public void save() {
        Book saved = books.save(new Book(id, title, author, year));
        PrimeFaces.current().dialog().closeDynamic(saved);
    }

    public void cancel() {
        PrimeFaces.current().dialog().closeDynamic(null);
    }

    public long getId() { return id; }
    public void setId(long id) { this.id = id; }
    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public String getAuthor() { return author; }
    public void setAuthor(String author) { this.author = author; }
    public int getYear() { return year; }
    public void setYear(int year) { this.year = year; }
    public boolean isFound() { return found; }
}

What happened, in order, on both implementations:

  1. The button posted an Ajax request (execute=f:...:edit because of process="@this"); the response told the client to open a dialog.

  2. The browser requested /bookshelf/df-edit.xhtml?id=2&pfdlgcid=<uuid> from inside the iframe. The pfdlgcid parameter ties the dialog to the request that opened it; the dialog title was the <title> of the view (Edit book). The view is a completely separate Faces view: it has its own view state and its own view-scoped beans, which the PrimeFaces documentation also states.

  3. In the dialog, Save was an Ajax post to df-edit.xhtml?pfdlgcid=...; closeDynamic(saved) made the dialog close.

  4. The host button’s dialogReturn behaviour then fired a second Ajax request to the host page (a ..._pfdlgcid= parameter), with jakarta.faces.behavior.event=dialogReturn; the listener onReturn(SelectEvent<Book>) received the saved book through getObject(), and the growl and the Last edited text were updated. Cancel (closeDynamic(null), immediate="true" so no validation blocks it) delivered a null object, which onReturn shows as cancelled.

  5. showMessageDynamic(new FacesMessage(...)) opened a small dialog with the summary and the detail text (Index rebuilt | It took 0 ms.).

Options. DialogFrameworkOptions.builder() has a method for each of these: modal (default false), draggable (true), resizable (true), closable (true), closeOnEscape (false), minimizable, maximizable, width, height (both auto), contentWidth (640), contentHeight, minWidth, minHeight, includeViewParams (false), headerElement, widgetVar, appendTo, dynamic, showEffect, hideEffect, position, fitViewport, responsive, focus, onShow, onHide, blockScroll, styleClass, iframeStyleClass, resizeObserver and resizeObserverCenter. The method names are those of the DialogFrameworkOptions.Builder class in the 16.0.0 jar, the defaults come from the documentation table. The documentation cautions that contentWidth="auto" and "100%" do not work well because the content is in an iframe; give a pixel width. modal(true) was used in the example.

Passing data. Parameters go to the dialog view as request parameters (the third argument; the example’s id, read by f:viewParam), which is the better choice: it is a bookmarkable, validated, string-typed input, and the view loads what it needs itself. The documentation also shows passing an object through the flash, with a DialogKeepFlashPhaseListener to keep it for the dialog’s request (the listener is registered by the jar, as seen above); the flash holds a value only for the next request and a large object belongs in the session or a service instead (see the flash page). Data comes back through closeDynamic(data) and the host’s dialogReturn behaviour; the object is available as SelectEvent#getObject().

The dialog view is a normal URL. GET /bookshelf/df-edit.xhtml?id=2 without pfdlgcid returned the edit page, the same form, with 200. Whoever can request the view can use it outside the dialog. Authorise in the view (a f:viewAction guard or the called service) exactly as for any other page, and never assume that the dialog is only reachable from the host. Calls to the dialog API in a non-Ajax request are ignored (documentation), so the opening buttons are Ajax buttons, as above.

p:blockUI

p:blockUI puts a translucent mask (optionally with content, such as a loading message) over a target while an Ajax request is running. block is a search expression for the target(s), trigger the id(s) of the component(s) whose requests show it (comma separated); with that wiring the mask appears at the start of the trigger’s Ajax request and disappears when it completes. blocked="true" blocks from the start, delay waits before showing, animate can be turned off, and the widget has show(), hide() and isBlocking().

blockui.xhtml — a mask over the panel while the slow action runs
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:p="primefaces">
<ui:composition template="/WEB-INF/templates/layout.xhtml">
  <ui:define name="title">BlockUI</ui:define>
  <ui:define name="content">
    <h:form id="f">
      <p:growl id="msgs"/>
      <p:panel id="slowPanel" header="Re-index the catalogue">
        <p:commandButton id="slow" value="Start" action="#{importStatusBean.reindex}" process="@form" update="msgs"/>
      </p:panel>
      <p:blockUI block="slowPanel" trigger="slow">Working, please wait...</p:blockUI>
    </h:form>
  </ui:define>
</ui:composition>
</html>

The mask was displayed 400 ms after the click with the text Working, please wait…​ while the action slept for 1.2 s, and was display: none afterwards, with the growl Re-index finished. Two limits from the documentation: it does not support absolutely or fixed positioned targets (a dialog, for example), and it is a visual aid only. A blocked page can still be posted to from script or another tab, so a mask is not protection against a double submit; guard the action itself (a token or an idempotent operation, as in the Post-Redirect-Get page). A page-wide busy indicator is the job of p:ajaxStatus.

p:poll

p:poll sends an Ajax request at an interval from the page, runs the listener (a method expression) and updates the components named in update. The interval is in seconds by default (interval="2"; intervalType="millisecond" changes the unit). autoStart="false" waits for PF('importPoll').start(); stop="#{...}" ends polling when the expression becomes true; stop() is the client call. onactivated and ondeactivated are client callbacks that may veto, async stops the requests from queueing, global decides whether p:ajaxStatus reacts, and process, partialSubmit, form behave as on any PrimeFaces Ajax source.

import-status.xhtml — check the progress of a long job every second
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:p="primefaces">
<ui:composition template="/WEB-INF/templates/layout.xhtml">
  <ui:define name="title">Import status</ui:define>
  <ui:define name="content">
    <h:form id="f">
      <h:outputText id="status" value="Checks: #{importStatusBean.checks}, finished: #{importStatusBean.done}"/>
      <p:poll id="poll" interval="1" listener="#{importStatusBean.check}" update="status"
              stop="#{importStatusBean.done}" widgetVar="importPoll"/>
    </h:form>
  </ui:define>
</ui:composition>
</html>
ImportStatusBean.java — the job, its checks and a slow action
package org.example.bookshelf.web;

import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import java.io.Serializable;

/** Stands in for a long-running catalogue import: every poll "checks" it, the third check finds it finished. */
@Named
@ViewScoped
public class ImportStatusBean implements Serializable {

    private int checks;
    private boolean done;

    public void check() {
        checks++;
        if (checks >= 3) {
            done = true;
        }
    }

    /** A slow action: the reason for p:blockUI. */
    public void reindex() throws InterruptedException {
        Thread.sleep(1200);
        FacesContext.getCurrentInstance().addMessage(null,
                new FacesMessage(FacesMessage.SEVERITY_INFO, "Re-index finished", null));
    }

    public int getChecks() { return checks; }
    public boolean isDone() { return done; }
}

Observed on both implementations: one request per second (execute=@all, render=f:status), the text going Checks: 1, Checks: 2, Checks: 3, finished: true, and exactly three requests in total: once the listener set done, the stop expression was true in the response and the client stopped (the poll is not in the update list; the stop worked anyway). The listener ran in Invoke Application.

Polling has costs and limits. Every poll is a full Faces request with a lifecycle and, with server-side state saving, a view state lookup; a view-scoped bean stays in use, and the requests count as session activity (not measured here). Use a long interval, stop as soon as the condition is met, keep the listener to a cheap read, and prefer server push for events that should reach the page the moment they happen: the f:websocket of the push page, which replaced the removed p:push. A p:poll is the right tool when the state is only visible by asking.

Security Notes for This Family

Security is part of correctness for these components, because most of them take strings from data and show them in other people’s browsers.

Escape what you show. Titles and labels are escaped by default (see cross-site scripting and the escape attribute). A hostile string <img src=x onerror=...>T was put in p:panel header, p:tab title (tab view and accordion), a p:submenu label, a p:menuitem value, a breadcrumb item, a steps item, the p:dialog header, the p:confirm header and message, a growl message and the text inside a p:blockUI; on Mojarra none of them created an img element. The one place where it did was p:menuitem escape="false", which ran the script (the counter of the handler was 1). escape="false" on a label that contains user data is an XSS bug; if markup is needed, build it from your own fixed strings or sanitise it. p:confirm has an escape attribute too (default true) with the same rule.

A hidden or disabled menu is not an authorization. rendered="false" and disabled="true" on items only change what a browser is told. The action method, the outcome target and the dialog view must each check the user’s role or the ownership of the data, for example with the role checks of the security page or an @RolesAllowed-style check in the service, and the menu model of Menus from a MenuModel may hide what the user cannot use for convenience only. The same holds for p:steps: a step is not reached because the step is clickable.

A client confirmation is not a protection. See p:confirmDialog, p:confirmPopup and p:confirm.

A dialog view is a URL. See A Launcher and a Dialog View. It is also a view in its own right for CSRF: its Ajax posts carry their own view state, and the protected-view rules of the CSRF section apply to it.

Do not trust the state the client posts. The details_collapsed and details_visible hidden fields, the active tab, the selected rows: all come from the browser. Use them for presentation, not for decisions. The edit dialog of this page therefore reads the book from the service by id on the server and the save action writes only the fields of the form, never a client-supplied identity (an editor still has to check that the user may edit book id, which this scratch application does not model).

Content Security Policy. With primefaces.CSP set to true (see the CSP section) the header was Content-Security-Policy: script-src 'self' 'nonce-...';, and the edit dialog (oncomplete and args), the confirmations, the dialog framework and the panel and tab Ajax behaviours all worked unchanged on Mojarra. Plain Faces command components are the part that does not work everywhere under this policy (see the CSP section: an h:commandButton with f:ajax did not update on Mojarra); use PrimeFaces components in such a page. Keep your own oncomplete and onclick to widget calls and args, as in the examples, and move real logic to a script file loaded with h:outputScript.

Mojarra Versus MyFaces

PrimeFaces components are the same code on both implementations; the Faces implementation supplies the lifecycle, state saving and Ajax runtime. Every scenario of this page (panel toggle and close, accordion and tab view events and veto, dynamic and cached tabs, the declarative and model menus, context menu, breadcrumb, steps, dialog with validation and args, global and local confirmations, blockUI, poll with stop, the dialog framework round trip) gave the same results on Mojarra 4.1.17 and MyFaces 4.1.4. The differences seen were cosmetic: generated ids (j_idt11 on Mojarra, j_id_h on MyFaces) and the string form of PhaseId (INVOKE_APPLICATION 5 against INVOKE_APPLICATION(5)). Only Mojarra was used for the hostile-string test, the Content Security Policy run, the dynamic dialog and the form-less poll described above. For the comparison of the two Faces implementations in general see Versions and Implementations.

What Changed per Version

Taken from the PrimeFaces migration guides (https://primefaces.github.io/primefaces/migrationguide/16_0_0.md and the guides of earlier versions).

Version Change that matters to this page

7.0

RequestContext was replaced by PrimeFaces.current(). RequestContext remains only as the internal PrimeRequestContext, which the 16.0.0 jar still contains and which you should not use.

8.0

Menus: DynamicMenuModel was removed (use BaseMenuModel/DefaultMenuModel), items are built with fluent builders (DefaultMenuItem.builder()) and MenuModel#addElement was deprecated in favour of getElements().add(...).

10.0

p:separator became p:divider; p:breadCrumb renders accessible (WAI-ARIA) markup.

11.0

p:dialog became responsive by default; the dialog framework can pass objects to the dialog through the flash; DefaultMenuItem defaults to global="true".

13.0

p:menu honours the expanded attribute of p:submenu (it used to ignore it, so set it to true where you want submenus open); p:confirm lost its default icon.

14.0

p:menubar: delay was renamed showDelay and hideDelay was added.

15.0

ThemeRoller themes are no longer supported (use the PrimeFaces themes); an empty menu or toolbar container is no longer rendered; p:separator was deprecated.

16.0

JSF and Java EE (javax.*) support was removed (the baseline is Faces 4.0 and newer); p:separator was removed; the dialog markup was restructured (ui-dialog-box); p:accordionPanel activeIndex became active and understands the key of a p:tab (activeIndex deprecated).

Faces itself: the dialogs, menus and polls use only the Ajax runtime, which was jsf.js until Faces 3.0 and faces.js since 4.0 (see the Ajax page). This page does not depend on Faces 5.0, which is still a milestone.

Where the Books Are Outdated

The PrimeFaces books of the 2013-2015 era (PrimeFaces Cookbook, second edition, for PrimeFaces 5.1/5.2 on JSF 2.2) cover these components in their layout, menus and advanced chapters. Their Ajax and component ideas still hold; the following do not:

  • The request context. They call the Ajax helper methods (callback parameters, executing script, the dialog framework) through RequestContext; today everything goes through PrimeFaces.current() (current().ajax().addCallbackParam(...), current().dialog(), current().executeScript(...)). I did not check the older method names of the dialog framework against the books, only the 16.0.0 ones above.

  • Themes and icons. The ThemeRoller themes and the jQuery UI icon classes (ui-icon-...) of their examples are replaced by the PrimeFaces themes and PrimeIcons (pi pi-...); the migration guide says legacy ThemeRoller themes are unsupported since 15 and do not render correctly. Some structural class names (ui-icon inside the panel header) remain.

  • Removed or renamed components and attributes. p:separator is gone, the p:menubar delay attribute was renamed, activeIndex of the accordion is deprecated, and the menu model is built with builders rather than constructors and addElement.

  • The old JSF base. Their pages use the java.sun.com or xmlns.jcp.org namespaces, javax.faces and @ManagedBean backing beans, none of which exist in Faces 4 (and PrimeFaces 16 no longer supports JSF 2.x); the examples above use CDI @Named, jakarta.* and the jakarta.faces.* URNs.

  • Push. The book’s p:push/Atmosphere approach to live updates was removed; poll is still available, and push is f:websocket or OmniFaces o:socket.

Pitfalls

Pitfall What to do

update="msgs" from inside an accordion, table or repeat fails the page

Use an absolute id (:form:msgs) or a search expression that resolves from the naming container; see What These Components Have in Common.

A button or menu item without process posts @all

Say process="@this" (or the ids you need) for row actions.

A dialog’s inputs are submitted with the wrong form, or nothing happens

With appendTo, the dialog leaves its form in the DOM; give the dialog’s content its own h:form.

A dialog closes although validation failed

Do not hide it from onclick; hide it in oncomplete after testing args, as in p:dialog.

p:ajax event="tabChange" updates the tab view or accordion itself

Update the messages or an inner container; the component breaks when it or a parent is replaced (documented).

The tab controller refuses a tab and the tabChange listener still changes data

The listener ran first in the test; make the listener apply the same rule.

A closed panel or tab comes back after a reload

Store the decision (a bean property, or a rendered flag) if it must survive.

Nothing happens in the dialog framework

Register the three handlers in faces-config.xml (Registration); there is no log message otherwise.

showMessageInDialog does not compile

The 16 API is showMessageDynamic.

The dialog view works as a stand-alone page for anyone

It is a URL: guard it like one (A Launcher and a Dialog View).

escape="false" on a menu label

XSS if any part of the text is user data; leave it at the default.

A poll or menu action does nothing, silently

No form around it; put it in an h:form (What These Components Have in Common).

p:poll runs forever or on a hidden tab

Bind stop to a state the listener sets, or stop it from script when the user leaves.

widgetVar the same as a component id

Choose a distinct, unique name; PF('name') looks the widget up by it.

A menu item with both url and action

url wins and the item is a link; use one of them.