Acumatica · Modernui

Acumatica Modern UI Screen — From Scratch

Acumatica Modern UI Screen — From Scratch is one of those Acumatica topics that is both obvious and subtle. The Modern UI is the default since 2025 R2, the Mobile Framework is the.

John Kihiu12 min read

Building a Modern UI screen from a blank slate — not extending an existing one, a genuinely new screen for a genuinely new graph — has more moving parts than the marketing material suggests, but they compose in a predictable order. This is the sequence I actually follow, using a simple "Expense Approval Queue" screen as the running example, from graph to sitemap entry.

Step 1: the graph exists independent of any UI — build and test it first

A genuinely useful habit that pays off regardless of which UI you'll eventually build: write and unit-test the PXGraph before touching any frontend code at all. The graph doesn't know or care whether Modern UI or classic ASPX will eventually render it — it's the same PXSelect views, actions, and event handlers either way:

C#
public class ExpenseApprovalEntry : PXGraph<ExpenseApprovalEntry>
{
    public PXSelect<ExpenseClaim,
        Where<ExpenseClaim.status, Equal<ExpenseClaimStatus.pendingApproval>>> PendingClaims;

    public PXAction<ExpenseClaim> Approve;
    [PXUIField(DisplayName = "Approve")]
    [PXButton]
    protected virtual IEnumerable approve(PXAdapter adapter)
    {
        foreach (ExpenseClaim claim in adapter.Get<ExpenseClaim>())
        {
            claim.Status = "A";
            PendingClaims.Update(claim);
        }
        Actions.PressSave();
        return adapter.Get();
    }
}

Step 2: the TypeScript screen class declares the view shape

In FrontendSources, a new screen class uses @graphInfo to bind to the graph by its full type name and primary view, then declares view factories mirroring the graph's PXSelect shape:

TYPESCRIPT
import { createCollection, graphInfo, PXScreen, PXView, createField, PXFieldState } from "client-controls";

@graphInfo({ graphType: "MyNamespace.Graphs.ExpenseApprovalEntry", primaryView: "PendingClaims" })
export class ExpenseApprovalScreen extends PXScreen {
  PendingClaims = createCollection(ExpenseClaimRow);
}

class ExpenseClaimRow extends PXView {
  ClaimNbr = createField<PXFieldState<string>>({});
  EmployeeName = createField<PXFieldState<string>>({});
  ClaimTotal = createField<PXFieldState<number>>({ displayFormat: "0.00" });
  Status = createField<PXFieldState<string>>({});
}

Step 3: layout markup composes qp-* components against the view

A separate layout definition (HTML-flavored markup using the qp-* web component family) declares the actual visual structure — a grid bound to PendingClaims, an approve button bound to the graph's Approve action:

HTML
<qp-grid view="PendingClaims" adjust-page-size="true">
  <qp-grid-column field="ClaimNbr"></qp-grid-column>
  <qp-grid-column field="EmployeeName"></qp-grid-column>
  <qp-grid-column field="ClaimTotal"></qp-grid-column>
  <qp-grid-column field="Status"></qp-grid-column>
</qp-grid>
<qp-toolbar>
  <qp-button action="Approve" caption="Approve Selected"></qp-button>
</qp-toolbar>

Step 4: the npm build compiles TypeScript to the bundle the browser loads

The FrontendSources project's build (npm/webpack, wired into the platform's own build pipeline) compiles every registered screen's TypeScript into the JavaScript bundles the app actually serves. During active development this runs in watch mode for fast iteration; for deployment, the compiled output packages into the customization project alongside your graph and DAC assemblies — Modern UI screens deploy through the same customization mechanism as everything else, not a separate release process.

A screen ID needs registering in three places before it's reachable — miss one and you get a confusing blank result

The screen needs: a screen ID (e.g. EP301500-style, following the module's numbering convention) registered against the graph type; a sitemap entry (Site Map screen, same as any classic screen) pointing at that screen ID; and the compiled TypeScript bundle actually containing the screen class with a matching @graphInfo registration. Miss the sitemap entry and the screen exists but nobody can navigate to it. Miss the bundle registration and the sitemap entry resolves to a blank or error page. I've chased both of these independently on first-time Modern UI screens before learning to check all three every time.

Step 5: test save, validation, and action invocation end to end, not just that it renders

A screen rendering correctly proves the TypeScript-to-graph binding works for reads; it proves nothing about writes. Before calling a new screen done, I explicitly test: editing a bound field and confirming the server-side FieldUpdated/RowPersisting logic fires as expected, invoking the custom action button and confirming it reaches the graph's action method, and a full save-and-reload cycle to confirm nothing about the round trip is silently dropping data. Screens that "look right" in a first pass but haven't been through this checklist are where most first-Modern-UI-screen bugs surface during actual client testing.

Wrapping up

Building a Modern UI screen from scratch is graph first (independent of any UI concerns), then a TypeScript screen class mirroring the graph's view shape with @graphInfo, then layout markup composing qp-* components against those views, then a build that bundles it all — and three separate registration points (screen ID, sitemap entry, bundle) that all need to line up before the screen is actually reachable. The graph-first discipline is the part experienced classic-UI developers most often skip when they're excited about a new UI framework, and it's the part that saves the most debugging time later.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.