Building Your First PCF Control for Power Apps
Sometimes a standard Power Apps control is almost right but not quite production-grade for the experience you need. You need richer rendering, tighter interaction, or a reusable component that behaves consistently across apps. That is where the PowerApps Component Framework earns its place.
PCF is for controls that deserve engineering discipline
Use PCF when configuration is no longer enough. The PowerApps Component Framework lets you build custom controls using TypeScript, a manifest, and platform lifecycle methods. The control can run in model-driven apps and canvas apps, depending on how you define and package it.
PCF is not the first answer for every UI problem. Try modern controls, canvas components, custom pages, command bar customization, and low-code configuration first. Reach for PCF when the behavior must be reusable, tested, performant, and closer to web component engineering than formula authoring.
Examples include masked inputs, timeline visualizations, compact status indicators, custom map interactions, advanced sliders, rating controls, and controls that integrate with approved JavaScript libraries. The value is not just prettier UI. The value is a governed control that ships through solutions and can be reused by multiple makers.
The tooling setup is small but specific
Install the prerequisites before generating files. You need Node.js, npm, the Power Platform CLI, and an environment where you can authenticate with pac auth. Use a supported Node version for the current PCF tooling and keep it aligned with your organization’s build agents.
node --version
npm --version
pac --version
pac auth create --url https://contoso-dev.crm.dynamics.com
Create a workspace folder under your solution source tree, then initialize the control. The namespace and name become part of the generated structure, so choose names you can live with in source control and solution components.
pac pcf init --namespace AugmentedDev --name StatusBadge --template field
npm install
npm run build
The generated project includes ControlManifest.Input.xml, index.ts, styling, generated type definitions, and build configuration. Most first controls only need serious edits in the manifest, index.ts, and CSS.
The manifest is the contract with Power Apps
Treat the manifest as the public API. It declares the control namespace, version, display names, bound properties, resources, feature usage, and supported platform behavior. If you bind to a Dataverse column, the manifest describes what type of value the framework passes into your control.
<manifest>
<control namespace="AugmentedDev" constructor="StatusBadge" version="1.0.0" display-name-key="StatusBadge" description-key="Shows a colored status badge" control-type="standard">
<property name="statusText" display-name-key="Status" description-key="Current status text" of-type="SingleLine.Text" usage="bound" required="true" />
<resources>
<code path="index.ts" order="1" />
<css path="css/StatusBadge.css" order="1" />
</resources>
</control>
</manifest>
Keep the first version boring. One bound text property, one visual output, no external service call, no ambitious rendering library. Once the lifecycle is familiar, the next control can be more interesting.
The TypeScript lifecycle is where the control behaves
Understand the four core methods. init runs once when the control is created. updateView runs when context changes and should render the current state. getOutputs returns values for output properties when your control changes data. destroy cleans up event handlers, timers, subscriptions, and DOM references.
import { IInputs, IOutputs } from "./generated/ManifestTypes";
export class StatusBadge implements ComponentFramework.StandardControl<IInputs, IOutputs> {
private container!: HTMLDivElement;
public init(
context: ComponentFramework.Context<IInputs>,
notifyOutputChanged: () => void,
state: ComponentFramework.Dictionary,
container: HTMLDivElement
): void {
this.container = container;
this.container.className = "status-badge";
}
public updateView(context: ComponentFramework.Context<IInputs>): void {
const value = context.parameters.statusText.raw ?? "Unknown";
this.container.textContent = value;
this.container.dataset.status = value.toLowerCase().replace(/\s+/g, "-");
}
public getOutputs(): IOutputs {
return {};
}
public destroy(): void {
this.container.replaceChildren();
}
}
Do not do expensive work in every updateView call. The framework can call it frequently. Cache DOM references, diff meaningful values, and clean up after yourself.
Local testing catches control mistakes before solution packaging
Use the test harness for fast feedback. The generated project can run locally and display the control with configurable inputs. It is not a perfect simulation of model-driven or canvas runtime behavior, but it catches rendering errors, basic property handling, and TypeScript issues quickly.
npm start
npm run build
After local validation, push to a development environment for real app testing. pac pcf push is convenient during early development because it creates a temporary solution behind the scenes.
pac auth select --index 1
pac pcf push --publisher-prefix adv
Use this for inner-loop testing, not as your final ALM path. Production-bound controls should be packaged in a proper solution and promoted through managed solution deployment.
Solutions make the control usable by makers
Package the control like any other application component. Create or use an existing solution, add the PCF project reference, build, and import the solution into the target environment. In model-driven apps, makers can add the control to a column on a form. In canvas apps, the control can be enabled and inserted where supported.
| Artifact | Purpose | Senior engineer check |
|---|---|---|
| Manifest | Defines properties and resources | Names, types, versioning are stable |
index.ts | Implements lifecycle behavior | No leaks, no repeated heavy rendering |
| CSS | Styles the rendered control | Scoped enough to avoid app-wide impact |
| Solution | ALM container | Managed for test and production |
| Publisher | Prefix and ownership | Matches team convention |
The first PCF control should teach the lifecycle, not prove every JavaScript skill you have. Keep the contract tight, render predictably, package through a solution, and you will have a control that feels native instead of a one-off script hiding inside Power Apps.
Keep reading
Securing Power Pages with Dataverse Table Permissions and Web Roles
Secure Power Pages data with Dataverse table permissions, access scopes, web roles, column profiles, and testing practices that prevent data leaks.
Canvas vs Model-Driven vs Power Pages: Choosing the Right Power Apps Type
Choose between canvas apps, model-driven apps, and Power Pages using a practical matrix for UX, data, licensing, governance, and audience fit.
Solving Power Apps Delegation Warnings for Large Dataverse Tables
Learn how Power Apps delegation works, why blue warnings matter, and the Dataverse patterns that keep large-table queries accurate and fast.
Newsletter
New posts, straight to your inbox
One email per post. No spam, no tracking pixels, unsubscribe anytime.
Comments
No comments yet. Be the first.