What Are Adaptive Card Extensions and Why They Matter
Adaptive Card Extensions — ACEs for short — are the native extension point for the Viva Connections dashboard. Unlike traditional web parts that render freely inside a SharePoint page, ACEs are surfaced as compact, data-dense cards on the Viva Connections dashboard in both Teams desktop and the Teams mobile app. This distinction is critical: ACEs are the primary way employees interact with corporate tools and data in the Viva experience without ever leaving Microsoft Teams.
Microsoft introduced ACEs with SPFx 1.13 and has been steadily expanding the model since. By early 2025, with Viva Connections widely adopted across enterprise tenants, ACEs have become the go-to pattern for surfacing leave balances, pending approvals, IT ticket counts, canteen menus, and dozens of other employee-facing workflows. In our projects, we've seen organisations replace entire intranet apps with lightweight ACEs because the card-plus-quick-view pattern satisfies 80% of daily employee interactions.
An ACE has two main surfaces. The Card View is the small tile on the dashboard — it shows a headline, a short piece of data, and one or two action buttons. The Quick View is a modal-like panel that opens when the user clicks the card; it renders a full Adaptive Card template with richer content, forms, and nested actions. Understanding this two-layer architecture from the start shapes how you design and scope your features.
ACE vs. Web Part: Choosing the Right SPFx Component
The question we hear most often from SharePoint developers moving into the Viva space is: "Should I build a web part or an ACE?" The answer depends on where and how the content will be consumed. Web parts live on SharePoint modern pages and are designed for extended reading and interaction — think dashboards, document libraries, rich content pages. ACEs live on the Viva Connections dashboard and are designed for glanceable information and single-action workflows.
A useful rule of thumb: if an employee needs more than one click to complete a task, and that task involves a meaningful UI (multi-step forms, data grids, charts), build a web part. If the primary value is showing one key number or status and offering a single action (approve, acknowledge, navigate), build an ACE. In our experience, the two often coexist — the ACE surfaces the summary, and a link inside the Quick View opens a full web part page for deeper work.
There is also a performance consideration. ACEs are loaded in the Viva Connections dashboard context, which runs inside Teams. Every ACE you deploy increases the Teams shell's load time. Microsoft recommends keeping individual ACE bundles under 100 KB. Web parts have more relaxed constraints because they only load when the user navigates to the specific SharePoint page. Keep this in mind when deciding how much logic to put inside your ACE versus delegating to a separate page experience.
You can target both Teams desktop and mobile with a single ACE codebase — ACEs are responsive by design. Test on both surfaces before releasing to production.
Scaffolding Your First ACE with Yeoman
ACEs are scaffolded the same way as web parts — through the Yeoman SharePoint generator. You'll need Node.js 18.x, the @microsoft/generator-sharepoint package at version 1.18 or later, and the SPFx build chain. The key difference from a web part scaffold is the component type selection: choose "Adaptive Card Extension" when the generator prompts you.
# Ensure generator is up to date npm install -g @microsoft/generator-sharepoint # Create a new project directory and scaffold mkdir my-leave-balance-ace && cd my-leave-balance-ace yo @microsoft/sharepoint # Generator prompts: # ? What is your solution name? my-leave-balance-ace # ? Which type of client-side component to create? Adaptive Card Extension # ? What is your Adaptive Card Extension name? LeaveBalanceAce # ? Which template do you want to use? Generic Card Template
After scaffolding, your project contains a src/adaptiveCardExtensions/leaveBalanceAce/ folder with three key files: the main ACE class that extends BaseAdaptiveCardExtension, a CardView.ts file, and a QuickView.ts file. There is also a config/package-solution.json that controls the deployment target — you'll want to set "skipFeatureDeployment": true for tenant-wide availability.
Run gulp serve and open the Viva Connections workbench at https://<your-tenant>.sharepoint.com/_layouts/15/workbench.aspx. You'll see the scaffold card rendering. From here you can iterate on card views and quick views with live reload, which dramatically speeds up development compared to waiting for full deployments.
Building the Card View and Quick View
The Card View class controls what appears on the dashboard tile itself. It must return one of the pre-defined card view configurations — BasicCardView, ImageCardView, or PrimaryTextCardView — through its cardViewParameters getter. The PrimaryTextCardView is the most common; it shows a title, a primary text value, a secondary description, and up to two action buttons.
import { BasePrimaryTextCardView, IPrimaryTextCardParameters, IExternalLinkCardAction, IQuickViewCardAction, } from '@microsoft/sp-adaptive-card-extension-base'; import { QUICK_VIEW_REGISTRY_ID } from '../LeaveBalanceAceAdaptiveCardExtension'; export class CardView extends BasePrimaryTextCardView<ILeaveBalanceAceProps, ILeaveBalanceAceState> { public get cardViewParameters(): IPrimaryTextCardParameters { return { primaryText: `${this.state.annualLeave} days remaining`, description: `Sick: ${this.state.sickLeave} | Casual: ${this.state.casualLeave}`, title: this.properties.title, iconProperty: 'Leave', buttons: [ { title: 'View Details', action: { type: 'QuickView', parameters: { view: QUICK_VIEW_REGISTRY_ID }, } as IQuickViewCardAction, }, { title: 'Apply Leave', action: { type: 'ExternalLink', parameters: { target: 'https://mycompany.sharepoint.com/sites/HR/Lists/LeaveRequests/NewForm.aspx' }, } as IExternalLinkCardAction, }, ], }; } }
The Quick View uses the Adaptive Card schema — a JSON template — to render its content. The template() getter returns the raw Adaptive Card JSON, and the data getter provides the data object that binds to template placeholders. This separation of template and data is powerful: you can update the card structure without changing TypeScript code, and the Adaptive Card Designer at adaptivecards.io lets you prototype layouts visually before wiring them up.
{
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{
"type": "TextBlock",
"text": "Your Leave Balance",
"weight": "Bolder",
"size": "Large"
},
{
"type": "FactSet",
"facts": [
{ "title": "Annual Leave", "value": "${annualLeave} days" },
{ "title": "Sick Leave", "value": "${sickLeave} days" },
{ "title": "Casual Leave", "value": "${casualLeave} days" },
{ "title": "Next Holiday", "value": "${nextHoliday}" }
]
}
],
"actions": [
{
"type": "Action.OpenUrl",
"title": "Apply for Leave",
"url": "${applyLeaveUrl}"
}
]
}
Connecting Your ACE to Real SharePoint Data
An ACE that shows hardcoded data is a prototype. A production ACE fetches real data from SharePoint lists, the Microsoft Graph API, or a backend service. The recommended pattern is to perform all data fetching inside the onInit() method of the main ACE class, then call this.setState() to trigger a re-render of both the Card View and Quick View. You can also set up a periodic refresh using setInterval in onInit() for data that changes frequently, though be mindful of API throttling.
PnPjs is the cleanest way to hit the SharePoint REST API from an ACE. Configure it with the SPFx context in onInit() and your service calls become concise, type-safe one-liners. For Microsoft Graph, use the this.context.msGraphClientFactory.getClient('3') approach — it handles token acquisition transparently and works within the ACE lifecycle without additional MSAL configuration.
import { sp } from '@pnp/sp/presets/all'; protected async onInit(): Promise<void> { sp.setup({ spfxContext: this.context as any }); await this.loadLeaveData(); return super.onInit(); } private async loadLeaveData(): Promise<void> { const balance = await sp.web.lists .getByTitle('LeaveBalances') .items .filter(`EmployeeEmail eq '${this.context.pageContext.user.email}'`) .select('AnnualLeave', 'SickLeave', 'CasualLeave', 'NextHoliday') .top(1)(); if (balance.length > 0) { this.setState({ annualLeave: balance[0].AnnualLeave, sickLeave: balance[0].SickLeave, casualLeave: balance[0].CasualLeave, nextHoliday: balance[0].NextHoliday, }); } } public onCardSelection(): IQuickViewCardAction | IExternalLinkCardAction | undefined { return { type: 'QuickView', parameters: { view: QUICK_VIEW_REGISTRY_ID }, }; }
Deploying to the Viva Connections Dashboard
Deploying an ACE follows the same path as any SPFx solution: bundle, package, upload to the App Catalog, then approve and install. Run gulp bundle --ship followed by gulp package-solution --ship to produce your .sppkg file. Upload it to the tenant or site collection App Catalog. If you set skipFeatureDeployment: true in package-solution.json, the solution deploys tenant-wide automatically.
Once deployed, navigate to the Viva Connections dashboard at https://<your-tenant>.sharepoint.com/sites/<home-site>/_layouts/15/VivaConnectionsDashboard.aspx. Click Edit, then Add a card. Your ACE will appear in the card picker under the name you defined in your manifest. Add it to the dashboard, configure any exposed properties through the property pane, and save. The card is now live for all users who have the Viva Connections tab in their Teams desktop client.
For staged rollouts, use card audiences. In the dashboard editor, click the audience icon on the card and point it at an Azure AD group. Only members of that group will see the card — this is invaluable for piloting with 50 users before a company-wide release. In our projects, we always run a two-week pilot with the HR or IT team before pushing ACEs to all employees.
The Viva Connections home site must be set in the SharePoint admin center before ACEs can be added to the dashboard. Many tenants miss this step and wonder why the dashboard editor never appears.
Advanced Patterns: Multi-Step Quick Views and State Management
A single Quick View works for simple information display, but real-world ACEs often need multi-step flows — a list of pending approvals, then an approval detail, then a confirmation screen. ACEs support multiple registered Quick Views, and you navigate between them using the quickViewNavigator service that is available on the ACE base class. Register all views in the ACE's onInit() using this.quickViewNavigator.register(VIEW_ID, () => new MyView()) and navigate with this.quickViewNavigator.push(NEXT_VIEW_ID).
State management in ACEs follows a Redux-lite pattern: state is a plain TypeScript object, and you update it exclusively through this.setState(partial). Both Card View and Quick View re-render whenever state changes. This means you must design your state interface carefully — if Quick View navigation needs to track the currently selected item, store the selected item ID in state rather than in the view class itself. We've seen ACEs become difficult to maintain when developers start storing view-local state in class properties instead of the centralised ACE state.
For ACEs that need to handle user actions (approving a request, submitting a form), implement action handlers in the Quick View by handling Action.Submit in the onAction(action) method. You can call SharePoint or Graph APIs directly from the Quick View, then call this.setState() on the parent ACE to update the card view count — for example, decrementing the pending approval count after a successful approval.
Performance, Card Sizing, and Adaptive Card Schema Tips
ACE performance is measured along two axes: how fast the card renders on the dashboard (initial bundle load plus data fetch latency), and how responsive the Quick View feels when opened. For bundle size, use dynamic imports to load heavy dependencies only when the Quick View opens, not at dashboard init time. Lazy-loading PnPjs modules is a particularly effective optimisation; in our benchmarks, it cuts initial ACE bundle size by 30–40%.
Adaptive Card schema version matters. Viva Connections uses the Teams Adaptive Card renderer, which currently supports schema up to version 1.5. Avoid using schema 1.6 features (like Table elements) without checking compatibility — the card will simply not render and show an error state with no developer-friendly message in the dashboard. Always test against the Teams mobile app as well, since some layout containers that look fine on desktop collapse unexpectedly on smaller screens.
Card sizing is controlled by the cardSize property returned from cardViewParameters. The options are Medium (default, 2 columns) and Large (3 columns). Large cards have more visual real estate but occupy more dashboard space, which can frustrate users who want to see more cards on their home screen. Our recommendation is to default to Medium and let users choose Large through the card's property pane if the content warrants it. Exposing card size as a web part property gives teams the flexibility to decide per deployment.
Key Takeaways
ACEs are the native SPFx extension point for Viva Connections — use them for glanceable data and single-action workflows, not extended UI experiences.
The two-layer model (Card View + Quick View) maps naturally to a summary-then-detail UX pattern that works well on both Teams desktop and mobile.
Fetch data in onInit(), manage all state through this.setState(), and keep Quick View navigation state in the centralised ACE state object.
Use card audiences for phased rollouts — target an Azure AD pilot group before enabling the ACE for all employees.
Keep ACE bundles under 100 KB using dynamic imports, and test against Adaptive Card schema 1.5 compatibility in both Teams desktop and mobile before go-live.