Development

Modernizing model-driven apps: Modern theming, React PCF controls and ALM

September 24, 2026
Sabhat Qureshi
11 min read

Modernization is no longer optional

For years, a lot of model-driven apps could get away with looking like they did in 2018. That is changing. Microsoft has been modernizing the model-driven UI in waves, aligned with the Fluent design system, and the first wave, the so-called New Look, is no longer a toggle you can quietly leave off. That puts a practical question on the table for every Dynamics 365 Customer Engagement team: what do you do with the custom web resources, the classic themes and the home-grown controls that were designed for the old UI?

This article is the decision guide I wish I had on every modernization project. It covers what the modern look actually changes, how modern theming replaces classic themes, the difference between standard and virtual (React) code components, how to decide whether a Power Apps component framework (PCF) control is worth building at all, and how to package and version it so it survives ALM. If you need the basics of PCF itself first, start with my earlier PCF article or the Power Apps component framework overview.

What the modern look changes

Microsoft describes the modernization as a series of waves. Wave 1, the New Look, brings Fluent-based field controls, the floating command bar, refreshed forms, views and dashboards, and the Power Apps grid in place of the old read-only grid. According to Microsoft's documentation, the New Look is mandatory from the 2026 release wave 1 onwards and makers can no longer switch an app back to the classic look from the app designer (an admin-level environment setting to revert is still documented, but should not be relied on). Wave 2, the header and navigation refresh, adds a modern app header, a streamlined sitemap and more working space on forms, and is opt-in for now. Read the details in the modern, refreshed look for model-driven apps.

In practice, the out-of-the-box parts of your app will be fine. The risk sits in everything you built around them. Before you flip anything in production, walk through this checklist in a sandbox copy of production:

  • Form scripts that touch the DOM: Anything using document.getElementById, jQuery selectors or CSS injection against platform markup was never supported and is the first thing to break when the markup changes. Rewrite it against the Client API.
  • HTML web resources on forms: They still work, but they now look out of place next to Fluent controls. Decide per web resource whether to restyle it, replace it with a PCF control or remove it.
  • Classic themes: Classic theming is not honored by the modern look. Your corporate colors disappear until you recreate them as a modern theme.
  • Custom chart colors: The modern palette overrides custom chart colors unless you set CustomColorOverride="true" on the chart XML.
  • Editable grids and custom grid controls: Views now use the Power Apps grid. Check that custom grid controls and editable subgrids still behave as users expect, and consider whether the Power Apps grid already covers the requirement.

Modern theming replaces classic themes

Modern themes are not configured in the old Themes area. You create an XML web resource with a CustomTheme element (a base palette color, optionally a font, a logo web resource and app header colors), then point the Custom theme definition setting at that web resource and publish. The platform generates a 16-slot Fluent palette from your base color. The full attribute list is in use modern themes in Power Apps.

<!-- Web resource crx_modern-theme.xml (type: Data (XML)) -->
<CustomTheme
    basePaletteColor="#0F6CBD"
    font="'Segoe UI', sans-serif"
    logoWebResource="crx_company-logo"
    logoTooltip="CRX Partners">
  <AppHeaderColors
      background="#0B3A5E"
      foreground="#FFFFFF"
      backgroundHover="#0E4A78"
      foregroundHover="#FFFFFF"
      backgroundPressed="#082A45"
      foregroundPressed="#FFFFFF"
      backgroundSelected="#0D4269"
      foregroundSelected="#FFFFFF" />
</CustomTheme>

Because the theme is a web resource plus a setting definition, both can live in a solution and travel through your pipeline like any other component. That is a real improvement over classic themes, which were notoriously awkward to deploy consistently.

Consultant tip

Keep lockPrimary at its default (false) unless branding insists on the exact hex value. The default algorithm optimizes the generated palette for accessibility; forcing the seed color into the primary slot can produce combinations that fail contrast requirements. Whatever you choose, check app header contrast for every button state.

Standard vs virtual code components

Once the look is modern, any custom control that renders its own old-style UI stands out. That is where the distinction between the two kinds of code components matters:

AspectStandard controlVirtual (React) control
RenderingReceives a container div in init and manipulates the DOM itselfupdateView returns a React element; the platform renders it
React and FluentBundled into each control if you use themShared platform libraries, declared in the manifest
Bundle sizeLarger, every control ships its own copySmaller, the libraries are not packaged
Modern themingManual, via the fluentDesignLanguage theme tokensFluent v9 controls pick up the app theme automatically
Power PagesSupportedNot supported for React controls and platform libraries

Two consequences are easy to miss. First, you cannot convert a standard control into a virtual one by changing control-type in the manifest; Microsoft states explicitly that you need to create a new control from the React template and port your logic. Second, if the same control must also run in Power Pages, a virtual control is not an option today.

Building a virtual control with React and Fluent

Scaffolding uses the --framework react switch of pac pcf init. The generated project declares React and Fluent as platform libraries, so they are available at build time and provided by the host at runtime; model-driven apps may load a higher compatible version than the one you declare (React 17.0.2 and Fluent 9.68.0 at the time of writing). Supported versions and the exact rules are listed on React controls and platform libraries.

Scaffolding and the inner loop

These commands create a React field control, run it in the local test harness and push it to a development environment for a first test inside a real form:

# Make sure the Power Platform CLI is current
pac install latest

# Scaffold a React (virtual) field control and run npm install
pac pcf init -n PriorityInput -ns Crx -t field -fw react -npm

# Build and open the local test harness (rebuilds on save)
npm run build
npm start watch

# Inner loop only: push to a DEVELOPMENT environment
pac auth create --environment https://yourorg-dev.crm4.dynamics.com
pac pcf push --publisher-prefix crx

The manifest

The two differences from a standard control are control-type="virtual" and the platform-library elements. Fluent 8 and Fluent 9 cannot both be declared in the same manifest; if you do not use Fluent at all, remove that line.

<?xml version="1.0" encoding="utf-8" ?>
<manifest>
  <control namespace="Crx" constructor="PriorityInput" version="1.0.0"
           display-name-key="PriorityInput" description-key="PriorityInput_Desc"
           control-type="virtual">
    <property name="value" display-name-key="Value" description-key="Value_Desc"
              of-type="Whole.None" usage="bound" required="true" />
    <resources>
      <code path="index.ts" order="1" />
      <platform-library name="React" version="16.14.0" />
      <platform-library name="Fluent" version="9.46.2" />
    </resources>
  </control>
</manifest>

index.ts and the component

init receives no container, and updateView returns a React element. Keep index.ts thin: translate the PCF context into props, hold the current output value and call notifyOutputChanged. Everything visual lives in a plain React component that you can test in isolation.

// index.ts
import { IInputs, IOutputs } from "./generated/ManifestTypes";
import { PriorityInputView, IPriorityInputProps } from "./PriorityInputView";
import * as React from "react";

export class PriorityInput implements ComponentFramework.ReactControl<IInputs, IOutputs> {
    private notifyOutputChanged: () => void;
    private currentValue: number | null = null;

    public init(
        context: ComponentFramework.Context<IInputs>,
        notifyOutputChanged: () => void,
        state: ComponentFramework.Dictionary
    ): void {
        this.notifyOutputChanged = notifyOutputChanged;
    }

    public updateView(context: ComponentFramework.Context<IInputs>): React.ReactElement {
        this.currentValue = context.parameters.value.raw;
        const props: IPriorityInputProps = {
            value: this.currentValue,
            disabled: context.mode.isControlDisabled,
            onChange: (newValue: number | null) => {
                this.currentValue = newValue;
                this.notifyOutputChanged();
            },
        };
        return React.createElement(PriorityInputView, props);
    }

    public getOutputs(): IOutputs {
        return { value: this.currentValue ?? undefined };
    }

    public destroy(): void {
        // Nothing to clean up: the platform unmounts the React tree
    }
}

// PriorityInputView.tsx
import * as React from "react";
import { SpinButton, SpinButtonOnChangeData } from "@fluentui/react-components";

export interface IPriorityInputProps {
    value: number | null;
    disabled: boolean;
    onChange: (value: number | null) => void;
}

export const PriorityInputView: React.FC<IPriorityInputProps> = ({ value, disabled, onChange }) => (
    <SpinButton
        value={value}
        min={1}
        max={5}
        disabled={disabled}
        onChange={(_event, data: SpinButtonOnChangeData) => {
            const parsed = data.value ?? (data.displayValue ? Number(data.displayValue) : null);
            onChange(parsed !== null && !Number.isNaN(parsed) ? parsed : null);
        }}
    />
);

Because the control uses Fluent v9 components from the platform library, it shares the platform's React context and inherits the current theme, including your custom modern theme, without extra code. If you style non-Fluent elements, use the tokens from context.fluentDesignLanguage rather than hardcoded colors, and read them on every updateView call so theme changes are picked up.

When is a PCF control worth building?

Modernization projects tend to trigger a wish list of custom controls. Most of them should not be built. A code component is code you own: it needs a repository, a build, versioning, testing in every release wave and someone who still understands it in three years. My rule of thumb is to exhaust configuration first.

Configure first when

  • An out-of-the-box control exists: The Power Apps grid, the rich text editor and the standard input controls such as toggle, star rating and slider cover a surprising share of requests.
  • The need is visual consistency: If a web resource only exists to show colored status labels or branding, a modern theme, conditional business rules or a simpler form often do the job.
  • A custom page fits better: For a guided, multi-step interaction, a custom page built with low-code controls can be cheaper to maintain than a bespoke control.
  • It is used once: A control that appears on a single form for a handful of users rarely justifies the lifecycle cost.

Build a PCF control when

  • The interaction is genuinely reusable: The same input pattern appears on many forms, tables or apps.
  • You are replacing an HTML web resource: A field-bound control gets proper data binding, respects form state and disabled mode, and looks native in the modern UI, which an iframe never will.
  • Users work in it all day: High-traffic screens where a better input or visualization saves real effort are where the investment pays back.
  • The team can maintain it: Someone owns the TypeScript and React code, the build and the release cadence.

Standard or virtual?

For new controls aimed at model-driven or canvas apps, start with a virtual control and Fluent v9. Choose a standard control only when you need Power Pages support or a rendering library that does not fit the React model.

Packaging, versioning and ALM

pac pcf push is a development shortcut. It builds the control and deploys it to a temporary PowerAppsTools solution (or a solution you name with --solution-unique-name), bypassing the version checks. It is perfect for the inner loop and the wrong tool for anything downstream of development. For test, acceptance and production, build a solution project (cdsproj) that references the control and ship that output as part of your managed solution. The steps are described in package a code component in a solution file.

# From the control folder: bump the PATCH version in the manifest
pac pcf version --strategy manifest

# One-time: create a solution project next to the control
mkdir Solutions
cd Solutions
pac solution init --publisher-name CRXPartners --publisher-prefix crx
pac solution add-reference --path ..

# Release build = production-optimised control bundle
dotnet build --configuration Release
# or: msbuild /t:restore  then  msbuild /p:configuration=Release

A Release configuration produces a production build of the control, but pac pcf push and npm run build produce development builds unless you set PcfBuildMode to production in the pcfproj (or run npm run build -- --buildMode production). Never import a development build into downstream environments: the bundle is larger and slower. Also check whether the cdsproj SolutionPackageType should be Managed, Unmanaged or Both for your pipeline.

Versioning rules that save you a support ticket

  1. Increment at least the PATCH part of the control version in ControlManifest.Input.xml for every deployment; otherwise model-driven apps may keep serving the cached previous version.
  2. Use pac pcf version --strategy manifest to bump the patch, or pac pcf version --patchversion with your pipeline's build number.
  3. Keep MAJOR.MINOR of the control in sync with MAJOR.MINOR of the solution that ships it.
  4. Make sure the publisher prefix used for development matches the publisher of the solution that carries the control downstream.
  5. Run the build in your pipeline, not on a developer laptop, so every artifact is traceable to a commit.

One solution or a separate component solution?

If the control is shared across several apps or environments, or is developed on its own release cadence, put it in its own solution and let the app solutions depend on it. Import order then matters: the component solution must be installed first. If the control belongs to one app only, keeping it in the app's solution is simpler. Microsoft documents both approaches in the code components ALM guidance.

Common pitfalls

  • Forgetting the version bump: The import succeeds, nothing changes on screen and everyone loses an afternoon clearing caches.
  • Bundling React anyway: Importing a different React version or a full Fluent package in a virtual control defeats the purpose. Stay within the versions the platform supports.
  • Ignoring disabled and masked states: Read context.mode.isControlDisabled and field security information; a control that lets users edit read-only data is a bug, not a feature.
  • Hardcoded colors: They clash with the customer's modern theme and with future display options. Use Fluent v9 components or theme tokens.
  • Shipping pac pcf push builds: The PowerAppsTools solution is unmanaged and not meant for downstream environments, and the control it contains is a development build unless PcfBuildMode is set to production.
  • Testing only in the harness: The test harness is great for quick iterations, but always validate inside a real form with the modern look, on desktop and in the mobile app if users rely on it.

Conclusion

Modernizing a model-driven app is mostly about removing things: unsupported DOM scripts, classic themes, web resources that fight the Fluent UI. Configure the modern theme, lean on out-of-the-box controls, and reserve PCF for interactions that are reusable and genuinely valuable. When you do build, build virtual controls on the platform's React and Fluent libraries, package them through a solution project and treat versioning as part of the definition of done. That combination keeps your app looking native through the next waves rather than drifting back into looking dated.

Need help with your Dynamics 365 project?

I help organizations modernize their model-driven apps, from theming and web resource clean-up to building and shipping PCF controls.