TreeTable provides a highly customizable ajax paginator.
<div class="card">
    <h:form id="form">
        <p:treeTable value="#{ttPaginatorView.root}" var="document" paginator="true" rows="10">
            <p:column headerText="Name">
                <h:outputText value="#{document.name}"/>
            </p:column>
            <p:column headerText="Size">
                <h:outputText value="#{document.size} KB"/>
            </p:column>
            <p:column headerText="Type">
                <h:outputText value="#{document.type}"/>
            </p:column>
        </p:treeTable>
    </h:form>
</div>
package org.primefaces.showcase.view.data.treetable;
import org.primefaces.model.DefaultTreeNode;
import org.primefaces.model.TreeNode;
import org.primefaces.showcase.domain.Document;
import java.io.Serializable;
import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
@Named("ttPaginatorView")
@ViewScoped
public class PaginatorView implements Serializable {
    private TreeNode<Document> root;
    @PostConstruct
    public void init() {
        root = new DefaultTreeNode();
        for (int i = 0; i < 50; i++) {
            TreeNode node = new DefaultTreeNode(new Document("Node " + i, String.valueOf((int) (Math.random() * 1000)), "Document"), root);
            for (int j = 0; j < 5; j++) {
                new DefaultTreeNode(new Document("Node " + i + "." + j, String.valueOf((int) (Math.random() * 1000)), "Document"), node);
            }
        }
    }
    public TreeNode getRoot() {
        return root;
    }
}
package org.primefaces.showcase.domain;
import java.io.Serializable;
public class Document implements Serializable, Comparable<Document> {
    private String name;
    private String size;
    private String type;
    public Document(String name, String size, String type) {
        this.name = name;
        this.size = size;
        this.type = type;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getSize() {
        return size;
    }
    public void setSize(String size) {
        this.size = size;
    }
    public String getType() {
        return type;
    }
    public void setType(String type) {
        this.type = type;
    }
    //Eclipse Generated hashCode and equals
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        result = prime * result + ((size == null) ? 0 : size.hashCode());
        result = prime * result + ((type == null) ? 0 : type.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        Document other = (Document) obj;
        if (name == null) {
            if (other.name != null) {
                return false;
            }
        }
        else if (!name.equals(other.name)) {
            return false;
        }
        if (size == null) {
            if (other.size != null) {
                return false;
            }
        }
        else if (!size.equals(other.size)) {
            return false;
        }
        if (type == null) {
            if (other.type != null) {
                return false;
            }
        }
        else if (!type.equals(other.type)) {
            return false;
        }
        return true;
    }
    @Override
    public String toString() {
        return name;
    }
    public int compareTo(Document document) {
        return this.getName().compareTo(document.getName());
    }
}