
Sales App Extensibility
- 4 installs
- 39 repo stars
- Updated June 16, 2026
- vtex/ai-skills
Builds and deploys VTEX Sales App extensions for cart, PDP, and menu using React hooks and typed API integrations generated from OpenAPI specs.
About
Covers building, customizing, and deploying extensions for VTEX Sales App via a 7-step workflow. A developer uses it to add cart, PDP, and menu features using hooks like useCart and usePDP and to generate typed API integrations from OpenAPI or inline specs.
- Extension points for cart, PDP, and menu with useCart/usePDP/useCartItem hooks
- API documentation ingestion (OpenAPI/URLs/inline) to generate typed integrations
Sales App Extensibility by the numbers
- 4 all-time installs (skills.sh)
- +1 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,817 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/vtex/ai-skills --skill sales-app-extensibilityAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4 |
|---|---|
| repo stars | ★ 39 |
| Last updated | June 16, 2026 |
| Repository | vtex/ai-skills ↗ |
What it does
Builds and deploys VTEX Sales App extensions for cart, PDP, and menu using React hooks and typed API integrations generated from OpenAPI specs.
Files
Sales App Extensibility
When this skill applies
Use this skill when building, customizing, or deploying extensions for VTEX Sales App.
- Adding features to the cart page (promotions, loyalty, services)
- Adding features to the Product Detail Page (badges, recommendations, warranties)
- Adding features to the menu (user profile, navigation, metrics)
- Integrating external APIs into Sales App extensions
- Generating, scaffolding, or validating extension code for Sales App
Do not use this skill for:
- Regular FastStore storefront customization (use
faststore-storefront) - Building VTEX IO apps (use
vtex-io-*skills) - Sales App core development or framework modifications
Prerequisite: FastStore project
The project root must contain: biome.json, faststore.json, package.json, tsconfig.json, turbo.json.
If any are missing, STOP. The user must install FastStore first:
npx @vtex/fsp-cli init
# Prompt: "What is the application name?" → enter name or press Enter for default
cd <application-name> && yarnDocumentation: https://beta.fast.store/getting-started
Prerequisite: Sales App module
Inside the FastStore project, a Sales App workspace must exist (typically at packages/sales-app) with src/, package.json, and tsconfig.json. Check root package.json "workspaces" for a path containing sales-app. If missing, STOP:
yarn add @vtex/sales-app -D -W
npx fsp create
# Prompts: account name → "Sales App" → path (default or custom)
# Then add the path to root package.json "workspaces" array
yarn installDocumentation: https://beta.fast.store/sales-app/setting-up
Do NOT proceed to discovery or code generation until both prerequisites are confirmed.
Decision rules
Follow the mandatory 7-step workflow (Steps 0–6) in order. Do not skip steps.
| Step | Purpose | Gate |
|---|---|---|
| 0 | Check prerequisites | FastStore + Sales App installed → proceed; otherwise STOP |
| 1 | Discovery | Understand what the user wants to build |
| 2 | Requirements & Plan | Map requirements → generate plan → wait for user approval |
| 3 | Code Generation & Validation | Generate Component.tsx + Component.css (plain CSS, never .module.css) + index.tsx → validate |
| 4 | Documentation | Generate docs/<ExtensionName>.md explaining the extension |
| 5 | Local Testing | Provide dev commands and URLs |
| 6 | Build & Deploy | Build command → deployment guide |
Extension point selection
| Extension Point | Category | Available Hooks | Layout Shift |
|---|---|---|---|
cart.cart-list.after | Cart | useCart, useExtension | No |
cart.cart-item.after | Cart | useCart, useCartItem, useExtension | Yes |
cart.order-summary.after | Cart | useCart, useExtension | Yes |
pdp.sidebar.before | PDP | usePDP, useCart, useExtension | Yes |
pdp.sidebar.after | PDP | usePDP, useCart, useExtension | Yes |
pdp.content.after | PDP | usePDP, useCart, useExtension | Yes |
menu.item | Menu | useExtension | No |
menu.drawer-content | Menu | useCurrentUser, useExtension | No |
Hook availability
- useCart → all cart + PDP extensions
- useCartItem →
cart.cart-item.afteronly - useCurrentUser →
menu.drawer-contentonly - usePDP → PDP extensions only
- useExtension → all extension points
Template selection
- No API + no hooks → simple template
- No API + hooks → hook template
- API + no auth → API template
- API + VTEX IO proxy → IO proxy template (recommended)
- API + direct auth → direct auth template (insecure, warn user)
- API doc provided → generate TypeScript interfaces from extracted response shapes; no API doc → use
${DATA_INTERFACE}placeholder
API authentication strategy
1. Recommended: VTEX IO Proxy App — IO app stores keys server-side. Extension uses credentials: 'include' with a relative path (/_v/my-api/data). 2. Insecure: Direct Auth — Keys in frontend code, visible in browser. Testing/development only.
Hard constraints
Constraint: Component must return JSX.Element, never null
defineExtensions expects ExtensionPointComponent which returns Element, not Element | null.
Why this matters Returning null causes a TypeScript compilation error. The build will fail.
Detection return null in any component registered with defineExtensions.
Correct
export function MyExtension(): JSX.Element {
if (!data) return (<></>);
return <div>{data.value}</div>;
}Wrong
export function MyExtension(): JSX.Element | null {
if (!data) return null;
return <div>{data.value}</div>;
}Constraint: Guard optional properties before use
CartItem.manualPrice (number | undefined), CartItem.productRefId (string | undefined), CartItem.attachments (Attachment[] | undefined) must be guarded.
Why this matters TypeScript strict mode rejects accessing possibly-undefined values.
Detection item.manualPrice, item.productRefId, or item.attachments without ?., ??, &&, or != null.
Correct
const price = item.manualPrice ?? item.sellingPrice;
const refId = item.productRefId ?? 'N/A';
const count = item.attachments?.length ?? 0;Wrong
const price = item.manualPrice;
const refId = item.productRefId.toUpperCase();
const count = item.attachments.length;Constraint: useCartItem().item may be undefined
item from useCartItem() is CartItem | undefined.
Why this matters Accessing properties on undefined causes a runtime crash.
Detection Destructured item from useCartItem() used without if (!item) guard.
Correct
const { item } = useCartItem();
if (!item) return (<></>);
return <div>{item.name}</div>;Wrong
const { item } = useCartItem();
return <div>{item.name}</div>;Constraint: defineExtensions is required in index.tsx
Entry point must use defineExtensions from @vtex/sales-app.
Why this matters Without it, the build succeeds but no extensions render.
Detection Missing defineExtensions import or call in index.tsx.
Correct
import { defineExtensions } from '@vtex/sales-app';
import { MyExtension } from './components/MyExtension';
export default defineExtensions({ 'cart.cart-list.after': MyExtension });Wrong
import { MyExtension } from './components/MyExtension';
export default { 'cart.cart-list.after': MyExtension };Constraint: Extension point names must match exactly
IDs are a fixed set. Non-existent names silently fail.
Why this matters The extension renders nowhere. No build error — invisible at runtime.
Detection Any name not in: cart.cart-list.after, cart.cart-item.after, cart.order-summary.after, pdp.sidebar.before, pdp.sidebar.after, pdp.content.after, menu.item, menu.drawer-content.
Correct
defineExtensions({ 'cart.cart-list.after': MyExtension });Wrong
defineExtensions({ 'cart.list.after': MyExtension });Constraint: Hooks must be used in compatible extension points
Each hook has an available_in restriction.
Why this matters Hook context is only mounted for specific extension points. Using elsewhere throws runtime errors.
Detection useCartItem in PDP/menu. useCurrentUser in cart. usePDP in menu/cart-list.
Correct
// useCartItem in cart.cart-item.after ✓
defineExtensions({ 'cart.cart-item.after': ItemWarranty });Wrong
// useCartItem in pdp.sidebar.after ✗ — will fail at runtime
defineExtensions({ 'pdp.sidebar.after': ItemWarranty });Preferred pattern
Workflow overview
Step 0 — Check FastStore + Sales App prerequisites. STOP if missing.
Step 1 — Discovery. Detect use case from keywords, ask follow-up questions, determine API auth strategy. If the user provides API documentation (URL, OpenAPI/Swagger file, Markdown, or inline text), ingest it to extract endpoint details and response shapes — skip the equivalent manual questions. Validate extracted information with the user. Load the discovery reference for detailed question flows and the API Documentation Ingestion section.
Step 2 — Map requirements to extension point + hooks + template. Present plan listing the files to be created: components/<ComponentName>.tsx, components/<ComponentName>.css (plain CSS — never .module.css), and index.tsx. Wait for approval.
Step 3 — Generate <ComponentName>.tsx, <ComponentName>.css (plain CSS, never .module.css), and index.tsx.
Required references for this step (load before generating):
- code-templates-and-patterns.md — all component templates, the CSS template (with the full Sales App design system inlined: tokens, typography, spacing, responsive), the
index.tsxtemplate, type generation rules, and the custom fetch hook pattern. - extension-points-hooks-and-types.md — hook return types and TypeScript definitions.
- static-analysis-rules.md — sandbox security, CSS containment, and React performance rules from
@vtex/fsp-analyzer. Run these checks on all generated files. - design-guidelines.md — load when the extension renders any UI text (sentence case rule) or uses icons (Phosphor Icons).
If API documentation was ingested in Step 1, generate TypeScript interfaces from the extracted response shapes and use them in the component instead of the ${DATA_INTERFACE} placeholder. If the extension calls 2+ endpoints, extract fetch logic into custom hook(s). Validate against the 12-point checklist in code-templates-and-patterns.md. Fix all violations before presenting code; surface warnings to the user.
Step 4 — Generate docs/<ExtensionName>.md inside the Sales App package (create the docs/ folder if needed). Load the documentation template reference for the required 9-section structure and the markdown skeleton to fill in.
Step 5 — Provide local dev commands and test URLs. Load the dev/build/deploy reference.
Step 6 — Build command and deployment guide. Load the dev/build/deploy reference.
Reference Files
Load these on demand based on what the task requires. Do not load all of them upfront.
| File | Load when… |
|---|---|
| references/extension-points-hooks-and-types.md | Choosing an extension point, selecting hooks, looking up TypeScript types (CartItem, ProductSku, Totalizers, Attachment), or checking hook return values and availability per extension point |
| references/code-templates-and-patterns.md | Generating extension code — simple, hook, API, IO Proxy, or Direct Auth templates; CSS template with the full Sales App design system inlined (tokens, typography, spacing, responsive); index.tsx with defineExtensions; hook initialization; validation checklist |
| references/discovery-and-use-cases.md | Running Step 1 (Discovery) — use case detection keywords, follow-up questions, API auth decision tree, IO Proxy vs Direct Auth flow |
| references/local-dev-build-and-deploy.md | Running Steps 5–6 — dev server commands, test URLs, build command, common build errors, FastStore WebOps deployment, monitoring, rollback |
| references/static-analysis-rules.md | Validating generated code (Step 3) — sandbox security, CSS containment, and React performance rules from @vtex/fsp-analyzer; full rule catalog with violation IDs, detection patterns, and correct/wrong examples |
| references/design-guidelines.md | Writing UI text (sentence case rule) or using icons (Phosphor Icons). CSS-related design rules — tokens, typography, spacing, responsive — are inlined directly in the CSS template inside code-templates-and-patterns.md, so this file is not needed for CSS generation. |
| references/documentation-template.md | Writing the docs/<ExtensionName>.md file in Step 4 — the 9-section structure and markdown skeleton |
Common failure modes
- Skipping prerequisite checks — Generating code without FastStore/Sales App installed. Always confirm both before Step 1.
- Not presenting plan — User may want a different approach. Always confirm at Step 2 before generating code.
- Skipping documentation — Extension generated without
docs/<ExtensionName>.md. Load documentation-template.md for the required structure. - Inventing API response types — Generated interface doesn't match actual API. If documentation was provided, derive types from it; if not, ask the user for a sample JSON response.
- Ignoring provided API documentation — User provided a URL or file but agent asked manual questions anyway. Always check for documentation first and use the API Documentation Ingestion flow.
- Inline fetch with 2+ endpoints — Multiple
fetchcalls inside the component body. Extract into custom hook(s) inhooks/use{Purpose}.ts. - Placing code outside packages/sales-app/src/ — Files outside this path are not included in the build.
Code-level violations (DOM APIs, Node imports, eval, CSS containment, React performance, design tokens) are enforced by @vtex/fsp-analyzer and caught during Step 3 validation. See static-analysis-rules.md and design-guidelines.md.Review checklist
Workflow gates
- [ ] FastStore installed (biome.json, faststore.json, package.json, tsconfig.json, turbo.json)?
- [ ] Sales App module installed (src/, package.json, tsconfig.json in sales-app directory)?
- [ ] Discovery completed and use case identified?
- [ ] Execution plan approved by user?
- [ ] Extension point is valid (from the 8-point reference)?
- [ ] Hooks compatible with chosen extension point?
- [ ] Documentation generated at
docs/<ExtensionName>.md(9 sections, using documentation-template.md)? - [ ] If API documentation was provided, TypeScript interfaces match the documented response shape?
- [ ] If 2+ API endpoints used, fetch logic extracted into custom hook(s) in
hooks/? - [ ] Build passes:
yarn fsp build {account} sales-app? - [ ] Tested locally:
yarn fsp dev {account}?
TypeScript / runtime guards (not caught by fsp-analyzer)
- [ ] Component returns JSX.Element, never null?
- [ ] Optional properties guarded (manualPrice, productRefId, attachments)?
- [ ] useCartItem().item checked for undefined?
- [ ] defineExtensions configured in index.tsx?
All other code-level rules (sandbox APIs, CSS containment, React performance, design tokens) are enforced by @vtex/fsp-analyzer and checked during Step 3. See static-analysis-rules.md and design-guidelines.md.Related skills
faststore-storefront— storefront customization outside Sales Appvtex-io-app-contract— building VTEX IO proxy apps for secure API integration
Reference
- Sales App setup: https://beta.fast.store/sales-app/setting-up
- FastStore getting started: https://beta.fast.store/getting-started
- VTEX Sales App documentation: https://help.vtex.com/en/tracks/instore-getting-started-and-setting-up
- VTEX IO app development: https://developers.vtex.com/docs/guides/vtex-io-documentation-developing-an-app
Code Templates and Patterns
Template Selection
| Condition | Template |
|---|---|
| No API + no hooks | Simple template |
| No API + hooks needed | Hook template |
| API + no authentication | API template |
| API + VTEX IO proxy app | IO Proxy template (recommended) |
| API + direct auth | Direct Auth template (insecure — warn user) |
Simple Extension (no hooks, no API)
import React from 'react';
import './${COMPONENT_NAME}.css';
/**
* ${COMPONENT_NAME}
* ${DESCRIPTION}
*
* Extension Point: ${EXTENSION_POINT}
*
* IMPORTANT: This component MUST always return a JSX.Element, never null.
* The defineExtensions type (ExtensionPointComponent) does not accept null.
* If you need to conditionally hide content, return an empty fragment: <></>
*/
export function ${COMPONENT_NAME}(): JSX.Element {
return (
<div className="container">
<div className="content">
${CONTENT}
</div>
</div>
);
}Extension with Hooks
import React from 'react';
import { ${HOOKS_IMPORT} } from '@vtex/sales-app';
import './${COMPONENT_NAME}.css';
/**
* ${COMPONENT_NAME}
* ${DESCRIPTION}
*
* Extension Point: ${EXTENSION_POINT}
* Hooks: ${HOOKS_LIST}
*/
export function ${COMPONENT_NAME}(): JSX.Element {
${HOOKS_USAGE}
return (
<div className="container">
<div className="content">
${CONTENT}
</div>
</div>
);
}Hook initialization patterns
// useCart — access cart data and mutations
const cart = useCart();
// cart.items, cart.value, cart.totalizers, cart.clientProfileData, cart.giftCards
// cart.addItem(), cart.removeItem(), cart.addCoupon(), cart.addGiftCard(), cart.sync()
// useCartItem — access individual cart item (cart.cart-item.after ONLY)
const { item, itemIndex, changeItem, changePrice } = useCartItem();
// ALWAYS check: if (!item) return (<></>);
// useCurrentUser — access logged-in user (menu.drawer-content ONLY)
const { name, email } = useCurrentUser();
// useExtension — access account and extension point context (ALL extension points)
const { account, extensionPoint } = useExtension();
// usePDP — access product data (PDP extensions ONLY)
const { productSku } = usePDP();
// productSku.id, productSku.name, productSku.price, productSku.listPriceExtension with API (no auth)
import React, { useState, useEffect } from 'react';
import { ${HOOKS_IMPORT} } from '@vtex/sales-app';
import './${COMPONENT_NAME}.css';
interface ${COMPONENT_NAME}Data {
// Generated from API documentation — see docs/<ExtensionName>.md for source
// Object fields → typed properties. Nested objects → separate interfaces. Optional fields → property?: Type
${DATA_INTERFACE}
}
export function ${COMPONENT_NAME}(): JSX.Element {
${HOOKS_USAGE}
const [data, setData] = useState<${COMPONENT_NAME}Data | null>(null)
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('${API_ENDPOINT}', {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
if (!response.ok) throw new Error('Failed to fetch data');
const result = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
};
fetchData();
}, [account]);
if (loading) {
return (
<div className="container">
<div className="loading">
<span className="spinner"></span>
<span>Loading...</span>
</div>
</div>
);
}
if (error) {
return (
<div className="container">
<div className="error"><span>Error: {error}</span></div>
</div>
);
}
if (!data) return (<></>);
return (
<div className="container">
<div className="content">
${CONTENT}
</div>
</div>
);
}Extension with IO Proxy (recommended for authenticated APIs)
The IO app handles the external API keys securely on the server side. The extension uses credentials: 'include' to forward session cookies.
Critical: The API endpoint must be a relative path (e.g., /_v/my-api/data). NEVER use a full URL like https://{account}.myvtex.com/.... The Sales App internal proxy resolves the domain automatically.
import React, { useState, useEffect } from 'react';
import { ${HOOKS_IMPORT} } from '@vtex/sales-app';
import './${COMPONENT_NAME}.css';
interface ${COMPONENT_NAME}Data {
// Generated from API documentation — see docs/<ExtensionName>.md for source
// Object fields → typed properties. Nested objects → separate interfaces. Optional fields → property?: Type
${DATA_INTERFACE}
}
export function ${COMPONENT_NAME}(): JSX.Element {
${HOOKS_USAGE}
const [data, setData] = useState<${COMPONENT_NAME}Data | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('${API_ENDPOINT}', {
method: '${API_METHOD}',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
${FETCH_BODY}
});
if (!response.ok) throw new Error('Failed to fetch data');
const result = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
};
fetchData();
}, [account]);
if (loading) {
return (
<div className="container">
<div className="loading">
<span className="spinner"></span>
<span>Loading...</span>
</div>
</div>
);
}
if (error) {
return (
<div className="container">
<div className="error"><span>Error: {error}</span></div>
</div>
);
}
if (!data) return (<></>);
return (
<div className="container">
<div className="content">
${CONTENT}
</div>
</div>
);
}Extension with Direct Auth (insecure — testing only)
⚠️ SECURITY WARNING: This template passes authentication keys directly in request headers. The keys are visible to anyone inspecting the browser's network requests. Use a VTEX IO proxy app in production.
import React, { useState, useEffect } from 'react';
import { ${HOOKS_IMPORT} } from '@vtex/sales-app';
import './${COMPONENT_NAME}.css';
/**
* ⚠️ SECURITY WARNING: Authentication keys exposed in frontend code.
* Move to a VTEX IO proxy app for production use.
*/
interface ${COMPONENT_NAME}Data {
// Generated from API documentation — see docs/<ExtensionName>.md for source
// Object fields → typed properties. Nested objects → separate interfaces. Optional fields → property?: Type
${DATA_INTERFACE}
}
export function ${COMPONENT_NAME}(): JSX.Element {
${HOOKS_USAGE}
const [data, setData] = useState<${COMPONENT_NAME}Data | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('${API_ENDPOINT}', {
method: '${API_METHOD}',
headers: {
'Content-Type': 'application/json',
// ⚠️ INSECURE: Authentication key exposed in frontend code
'${AUTH_HEADER_NAME}': '${AUTH_HEADER_VALUE}',
},
${FETCH_BODY}
});
if (!response.ok) throw new Error('Failed to fetch data');
const result = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
};
fetchData();
}, [account]);
if (loading) {
return (
<div className="container">
<div className="loading">
<span className="spinner"></span>
<span>Loading...</span>
</div>
</div>
);
}
if (error) {
return (
<div className="container">
<div className="error"><span>Error: {error}</span></div>
</div>
);
}
if (!data) return (<></>);
return (
<div className="container">
<div className="content">
${CONTENT}
</div>
</div>
);
}CSS Stylesheet
All extensions use plain CSS (not CSS modules). The full Sales App design system for CSS — tokens, typography, spacing grid, responsive breakpoints, and structure — is inlined below. Use this template as the single source of truth for any CSS generation.
CRITICAL — File naming: The CSS file MUST be named ${COMPONENT_NAME}.css — never ${COMPONENT_NAME}.module.css. The Sales App bundler does not support CSS modules. Import as a side-effect: import './${COMPONENT_NAME}.css'; — never as import styles from './${COMPONENT_NAME}.module.css';. Use className="container" string literals, not className={styles.container}.
Design system rules baked into this template
These rules apply to every generated extension CSS. The template below already complies — keep it that way.
| Topic | Rule |
|---|---|
| Tokens | All colors must reference --sa-color-* tokens declared on .container. Never use hardcoded hex values anywhere else. |
| Font family | 'VTEX Trust', -apple-system, BlinkMacSystemFont, sans-serif on .container, inherited by descendants. |
| Font weights | Only Regular (400), Medium (500), Semibold (600), Bold (700). |
| Font sizes (px) | Allowed scale only: 10, 12, 14, 16, 18, 20, 22, 24, 28, 32, 36, 40, 44, 48. Never 13, 15, 17, etc. |
| Typography roles | Section title = 16px/600 · Subtitle/label = 14px/500 · Body = 14px/400 · Caption = 12px/400 · Price = 18px/600. |
| Spacing | Multiples of 4px only (4, 8, 12, 16, 20, 24, 32, 40…). Never 10, 14, 18, 22. Applies to padding, margin, gap, height. |
| Containment | No position: fixed, no z-index: 9999, no :host, no :global, no @import. position: relative and sticky are safe. |
| Selectors | Use the unprefixed utility class names from this template (.container, .content, .row, .title, .button, etc.). The Sales App fsp-analyzer runs with transformNonCompliant: true, so unprefixed selectors emit a CSS_TRANSFORMED warning rather than a CSS_NAMESPACE_REQUIRED violation. Never use *, body, html, :root, head, main, #root, #__next — those are caught by CSS_GLOBAL_SELECTOR and remain blocking. |
| Keyframes | Prefix with ${COMPONENT_NAME}- (e.g., ${COMPONENT_NAME}-spin). This raises a CSS_TRANSFORMED warning (build still passes) — use sales-app-extension-${COMPONENT_NAME}-spin if you want a clean run. |
!important | Never. |
| Responsive | Use the breakpoint at (max-width: 743px) for mobile overrides. Sales App shell handles the rest. |
/**
* ${COMPONENT_NAME} Styles
* Sales App design system (tokens + typography + spacing + responsive) is fully inlined.
* UX writing: Use sentence case for all UI text. Never use ALL-CAPS.
*/
.container {
/* === Sales App design tokens — single source of truth ===
Always declare these on .container. Reference via var(--sa-color-*) in all other rules.
Never use hardcoded hex values in CSS rules. */
/* Primary action */
--sa-color-primary: #157BF4; /* Light Blue 800 */
--sa-color-primary-hover: #0366DD; /* Light Blue 900 */
/* Text */
--sa-color-text-primary: #1F1F1F; /* Neutral 1200 — high emphasis */
--sa-color-text-secondary: #5C5C5C; /* Neutral 600 — medium emphasis */
--sa-color-text-tertiary: #3D3D3D; /* Neutral 500 — low emphasis */
--sa-color-text-muted: #999999; /* Neutral 700 — disabled / hint */
/* Backgrounds */
--sa-color-bg: #FFFFFF; /* Neutral White */
--sa-color-bg-subtle: #F5F5F5; /* Neutral 100 */
--sa-color-bg-muted: #EBEBEB; /* Neutral 200 */
/* Borders */
--sa-color-border: #E0E0E0; /* Neutral 300 */
--sa-color-border-input: #D6D6D6; /* Neutral 400 */
/* Error (Red scale) */
--sa-color-error-bg: #FDF6F5; /* Red 50 */
--sa-color-error-border: #FFDFD9; /* Red 200 */
--sa-color-error-text: #EC3727; /* Red 800 */
/* Success (Green scale) */
--sa-color-success-bg: #EDFDF5; /* Green 50 */
--sa-color-success-text: #01905F; /* Green 800 */
/* Warning (Yellow / Orange scale) */
--sa-color-warning-bg: #FBF7D4; /* Yellow 50 */
--sa-color-warning-text: #E57001; /* Orange 700 */
/* Info (Light Blue scale) */
--sa-color-info-bg: #F1F8FD; /* Light Blue 50 */
--sa-color-info-text: #157BF4; /* Light Blue 800 */
padding: 16px;
background-color: var(--sa-color-bg);
font-family: 'VTEX Trust', -apple-system, BlinkMacSystemFont, sans-serif;
}
/* Layout */
.content { display: flex; flex-direction: column; gap: 12px; }
.row { display: flex; align-items: center; gap: 12px; }
.spaceBetween { justify-content: space-between; }
/* Typography — uses approved size scale (10, 12, 14, 16, 18, 20, 22, 24...) */
.title { font-size: 16px; font-weight: 600; color: var(--sa-color-text-primary); margin: 0; }
.subtitle { font-size: 14px; font-weight: 500; color: var(--sa-color-text-secondary); margin: 0; }
.text { font-size: 14px; color: var(--sa-color-text-tertiary); line-height: 1.5; }
/* Loading state */
.loading {
display: flex; align-items: center; justify-content: center;
gap: 8px; padding: 24px; color: var(--sa-color-text-secondary);
}
.spinner {
width: 20px; height: 20px;
border: 2px solid var(--sa-color-border); border-top-color: var(--sa-color-primary);
border-radius: 50%; animation: ${COMPONENT_NAME}-spin 1s linear infinite;
}
@keyframes ${COMPONENT_NAME}-spin { to { transform: rotate(360deg); } }
/* Error state */
.error {
padding: 16px; background-color: var(--sa-color-error-bg);
border: 1px solid var(--sa-color-error-border); border-radius: 8px;
color: var(--sa-color-error-text); font-size: 14px;
}
/* Button styles */
.button {
display: inline-flex; align-items: center; justify-content: center;
padding: 12px 16px; font-size: 14px; font-weight: 500;
border: none; border-radius: 6px; cursor: pointer;
transition: background-color 0.2s ease;
}
.buttonPrimary { background-color: var(--sa-color-primary); color: #FFFFFF; }
.buttonPrimary:hover { background-color: var(--sa-color-primary-hover); }
.buttonSecondary { background-color: var(--sa-color-bg-muted); color: var(--sa-color-text-tertiary); }
.buttonSecondary:hover { background-color: var(--sa-color-border); }
/* Card */
.card {
padding: 16px; background-color: var(--sa-color-bg-subtle);
border-radius: 8px; border: 1px solid var(--sa-color-border);
}
/* Badges */
.badge {
display: inline-block; padding: 4px 8px;
font-size: 12px; font-weight: 500; border-radius: 4px;
}
.badgeSuccess { background-color: var(--sa-color-success-bg); color: var(--sa-color-success-text); }
.badgeWarning { background-color: var(--sa-color-warning-bg); color: var(--sa-color-warning-text); }
.badgeInfo { background-color: var(--sa-color-info-bg); color: var(--sa-color-info-text); }
/* Input */
.input {
width: 100%; padding: 12px; font-size: 14px;
border: 1px solid var(--sa-color-border-input); border-radius: 6px;
outline: none; transition: border-color 0.2s ease;
}
.input:focus { border-color: var(--sa-color-primary); }
/* Price display */
.price { font-size: 18px; font-weight: 600; color: var(--sa-color-text-primary); }
.priceOld { font-size: 14px; color: var(--sa-color-text-muted); text-decoration: line-through; }
.priceDiscount { font-size: 14px; color: var(--sa-color-success-text); font-weight: 500; }
/* Responsive — Sales App breakpoints
Small (mobile) 320–743px → margin: 24px, no column grid
Medium (tablet) 744–1279px → host gutters: 16px (no override needed)
Large (desktop) 1280–1919px → host gutters: 20px (no override needed)
Extra large 1920px+ → host gutters: 20px (no override needed) */
@media (max-width: 743px) {
.container { padding: 12px; margin: 0 24px; }
}CSS file structure
Every extension CSS must follow this order:
1. Token declarations — --sa-color-* on .container 2. Layout helpers — .container, .content, .row, .spaceBetween 3. Typography — .title, .subtitle, .text 4. State — .loading, .spinner, @keyframes, .error 5. Component classes — .button, .card, .badge, .input, .price 6. Responsive — @media queries at the bottom
For UX writing rules (sentence case) and iconography (Phosphor Icons) used in .tsx, see design-guidelines.md.
index.tsx with defineExtensions
import { defineExtensions } from '@vtex/sales-app';
${COMPONENT_IMPORTS}
/**
* Sales App Extensions Entry Point
*
* Connects extension components to their target extension points.
* Each extension point can only have one component assigned to it.
*
* Available extension points:
* - cart.cart-list.after
* - cart.cart-item.after
* - cart.order-summary.after
* - pdp.sidebar.before
* - pdp.sidebar.after
* - pdp.content.after
* - menu.item
* - menu.drawer-content
*/
export default defineExtensions({
${EXTENSION_MAPPINGS}
});Validation Rules
After generating code, validate for these issues:
1. React import present — import React from 'react' is required for JSX compilation 2. Component exported — must have export function, export const, or export default 3. No null returns — return null is not allowed; use return (<></>) instead 4. Hook imports — all hooks must be imported from @vtex/sales-app 5. Optional property guards — manualPrice, productRefId, attachments must have null checks 6. useCartItem item check — if (!item) return (<></>) before accessing item properties 7. Hook/extension-point compatibility — each hook is only valid in specific extension points 8. API call handling — loading state and error handling must be present for any fetch() call 9. CSS class usage — CSS classes defined in the stylesheet should be used in the component 10. defineExtensions in index.tsx — entry point must import and call defineExtensions 11. Static analysis compliance — generated code must pass all fsp-analyzer sandbox security, CSS containment, and React performance rules. Load the static analysis reference to run the full check. Fix all violations before presenting code to the user; flag warnings to the user for review. 12. Design system compliance — the CSS file must follow the rules table at the top of the "CSS Stylesheet" section above: --sa-color-* custom properties declared on .container and used in all rules, 'VTEX Trust' font family, allowed font sizes (10, 12, 14, 16, 18, 20, 22, 24, 28, 32…), 4px-multiple spacing, scoped selectors, and the responsive override at (max-width: 743px). For UI text (sentence case) and icons (Phosphor), see design-guidelines.md.
API Type Generation from Documentation
When API documentation was ingested during Discovery (Step 1), generate TypeScript interfaces from the extracted response shapes. Apply these rules:
JSON to TypeScript conversion rules
| JSON value | TypeScript type |
|---|---|
"string" | string |
123 | number |
true / false | boolean |
null or sometimes absent | `type \ |
{ } (nested object) | Separate named interface |
[ ] (array of objects) | InterfaceType[] |
[ ] (array of primitives) | string[], number[], etc. |
Naming conventions
- Top-level response type:
{ComponentName}Data(replaces the${DATA_INTERFACE}placeholder) - Nested object type:
{ComponentName}{FieldName}(e.g.,LoyaltyHistoryEntry) - Request body type (POST/PUT):
{ComponentName}Request - Keep names PascalCase and domain-specific
Example
Given API response:
{ "points": 100, "tier": "gold", "expiresAt": null, "history": [{ "date": "2026-01-01", "amount": 10 }] }Generate:
interface LoyaltyHistoryEntry {
date: string;
amount: number;
}
interface LoyaltyData {
points: number;
tier: string;
expiresAt: string | null;
history: LoyaltyHistoryEntry[];
}If a field is documented as optional (not always present), use ?:
interface ProductRecommendationData {
id: string;
name: string;
price: number;
imageUrl?: string; // optional per API docs
badge?: string; // optional per API docs
}Multiple endpoints
If the extension calls more than one endpoint, generate a separate interface per endpoint response. Prefix with the endpoint purpose: {ComponentName}{Purpose}Data (e.g., LoyaltyBalanceData, LoyaltyRedemptionResponse).
Custom Fetch Hook Pattern
Use this pattern when the extension calls 2 or more endpoints or when the fetch logic is complex (chained requests, pagination, polling).
Decision rule
- 1 endpoint → inline
useEffectfetch inside the component (existing pattern). - 2+ endpoints → extract into one or more custom hooks in
hooks/use{Purpose}.ts.
Custom hook template
import { useState, useEffect } from 'react';
export function use${Purpose}(account: string) {
const [data, setData] = useState<${ResponseType} | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('${API_ENDPOINT}', {
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
});
if (!response.ok) throw new Error('Failed to fetch ${Purpose}');
const result: ${ResponseType} = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
};
fetchData();
}, [account]);
return { data, loading, error };
}Usage in component
import { use${Purpose} } from './hooks/use${Purpose}';
export function ${COMPONENT_NAME}(): JSX.Element {
const { account } = useExtension();
const { data: balance, loading: loadingBalance, error: errorBalance } = use${Purpose}(account);
const { data: history, loading: loadingHistory, error: errorHistory } = use${OtherPurpose}(account);
if (loadingBalance || loadingHistory) return <div className="loading">Loading...</div>;
if (errorBalance || errorHistory) return <div className="error">Error loading data</div>;
if (!balance || !history) return (<></>);
return (
<div className="container">
{/* render using balance and history */}
</div>
);
}File placement
Custom hooks go in packages/sales-app/src/hooks/use{Purpose}.ts. They must be TypeScript-only files (no JSX, no .tsx extension needed unless they return JSX).
Sales App Design Guidelines (non-CSS)
This reference covers two topics that apply to TSX content, not CSS:
1. UX writing rules — for any text rendered in JSX 2. Iconography — when the extension uses icons
For CSS: all design tokens, typography scale, spacing grid, and responsive breakpoints are inlined directly in the CSS template at code-templates-and-patterns.md §"CSS Stylesheet". That template is the single source of truth for any CSS generation. Do not invent or duplicate token values here.
---
1. UX Writing Rules
Apply to every string that appears in the UI (labels, buttons, messages, placeholders, alt text).
| Rule | Correct | Wrong |
|---|---|---|
| Sentence case | "Add to cart" | "ADD TO CART" |
| Proper nouns capitalized | "VTEX account" | "vtex account" |
| No all-caps | "Loyalty points" | "LOYALTY POINTS" |
| No system-style placeholders | "Your email" | "EMAIL ADDRESS" |
| No CSS uppercase transforms | (omit text-transform: uppercase) | text-transform: uppercase |
These rules derive from Google Material Design and Apple Human Interface Guidelines, adopted by VTEX.
---
2. Iconography
Library
All icons must come from [Phosphor Icons](https://phosphoricons.com/). Do not use Material Icons, Font Awesome, Heroicons, or any other library.
Installation
yarn add @phosphor-icons/reactSizes
| Usage | Size |
|---|---|
| Compact / inline | 16px |
| Standard / default | 24px |
| Large / featured | 32–48px |
Always use sizes within 16px to 48px.
Weights
Use Regular or Bold weight variants. Keep the weight consistent within a single component.
Usage example
import { ShoppingCart, Star, Warning } from '@phosphor-icons/react';
export function MyExtension(): JSX.Element {
return (
<div className="container">
<ShoppingCart size={24} />
<span>Cart items</span>
</div>
);
}---
3. Quick checklist
Before presenting generated .tsx to the user, verify:
- [ ] All UI text in sentence case (no ALL-CAPS, no system-style placeholders)?
- [ ] All icons imported from
@phosphor-icons/react? - [ ] Icon sizes between 16px and 48px (24px default)?
- [ ] Icon weights consistent within the component?
For CSS verification (tokens, fonts, spacing, responsive), see code-templates-and-patterns.md §"Design system rules baked into this template".
Discovery Flow and Use Cases
Use Case Detection
Detect the use case from keywords in the user's description:
| Use Case | Keywords | Complexity | Requires API | Requires Hooks |
|---|---|---|---|---|
| Static Information | message, warning, banner, badge, text, static, fixed | Simple | No | No |
| Loyalty Program | loyalty, points, benefit, redeem, cashback, program | Medium | Yes | Yes |
| Additional Services | warranty, insurance, installation, assembly, service, extra | Medium | Yes | Yes |
| Product Recommendations | recommendation, suggestion, related, complementary, cross-sell, upsell | High | Yes | Yes |
| Custom Discounts | discount, coupon, promotion, price, voucher | High | Yes | Yes |
| User Profile Display | profile, user, seller, menu, drawer | Simple | No | Yes |
Discovery Questions by Use Case
Static Information
1. Content type: Text message, Banner/Card, Badge/Tag, List of information, or Alert/Warning? 2. Location: Which extension point?
cart.cart-list.after— After the cart items listcart.order-summary.after— After the order summarypdp.sidebar.before— Before the PDP sidebarpdp.sidebar.after— After the PDP sidebarpdp.content.after— After the PDP content
3. Content: What text/information to display?
Recommended extension points: cart.cart-list.after, cart.order-summary.after, pdp.sidebar.before Required hooks: none Template: Simple
Loyalty Program
0. API documentation (optional, recommended): Do you have API documentation to share? URL, OpenAPI/Swagger JSON/YAML, Markdown file path, or paste the spec inline. If yes, skip questions 2–5 — the agent will extract endpoint, parameters, and response types automatically. 1. Data source: Own external API, VTEX IO app, VTEX Master Data, Third-party API, or Not sure yet? 2. API endpoint: Path (e.g., /_v/my-loyalty/points) — only if external data source and no docs provided 3. HTTP method: GET, POST, PUT, or DELETE? — skip if API docs provided 4. Request params: Parameters, query strings, or request body to send — skip if API docs provided 5. Response format: JSON structure of the API response — skip if API docs provided 6. Features: Display points balance, Show earnings with purchase, Allow points redemption, Display customer level 7. Location: cart.order-summary.after or menu.drawer-content 8. Interaction: View only or Interactive (redeem points, apply discounts)?
Recommended extension points: cart.order-summary.after, menu.drawer-content Required hooks: useCart, useExtension, useCurrentUser Template: API (IO Proxy or Direct Auth)
Additional Services
0. API documentation (optional, recommended): Do you have API documentation to share? URL, OpenAPI/Swagger JSON/YAML, Markdown file path, or paste the spec inline. If yes, skip questions 3–6 — the agent will extract endpoint, parameters, and response types automatically. 1. Service types: Extended warranty, Insurance, Installation, Assembly, Customization, Other 2. Data source: External API, VTEX IO app, Fixed list in code, or VTEX Catalog? 3. API endpoint: Path — only if external data source and no docs provided 4. HTTP method: GET, POST, PUT, or DELETE? — skip if API docs provided 5. Request params: Parameters or request body — skip if API docs provided 6. Response format: JSON structure of the API response — skip if API docs provided 7. Location: cart.cart-item.after (per item) or pdp.sidebar.after (product page) 8. Pricing model: Fixed price, Percentage of product, or Calculated by API?
Recommended extension points: cart.cart-item.after, pdp.sidebar.after Required hooks: useCart, useCartItem, usePDP Template: Hook or API (depending on data source)
Product Recommendations
0. API documentation (optional, recommended): Do you have API documentation to share? URL, OpenAPI/Swagger JSON/YAML, Markdown file path, or paste the spec inline. If yes, skip questions 3–6 — the agent will extract endpoint, parameters, and response types automatically. 1. Recommendation type: Frequently bought together, Accessories, Similar products, Upgrades 2. Data source: Own API, VTEX Intelligent Search, Fixed rules, or Third-party API? 3. API endpoint: Path — only if external data source and no docs provided 4. HTTP method: GET, POST, PUT, or DELETE? — skip if API docs provided 5. Request params: Parameters or request body — skip if API docs provided 6. Response format: JSON structure of the API response — skip if API docs provided 7. Location: cart.cart-list.after or pdp.content.after 8. Quick add: Can the seller quickly add recommended products to the cart? 9. Display count: 3, 4, 6 products, or customizable?
Recommended extension points: cart.cart-list.after, pdp.content.after Required hooks: useCart, usePDP, useExtension Template: API (IO Proxy or Direct Auth)
Custom Discounts
0. API documentation (optional, recommended): Do you have API documentation to share? URL, OpenAPI/Swagger JSON/YAML, Markdown file path, or paste the spec inline. If yes, skip questions 3–6 — the agent will extract endpoint, parameters, and response types automatically. 1. Discount types: Discount coupon, Manual seller discount, Volume discount, Special promotion 2. Validation source: Own promotions API, VTEX Promotions, or Fixed coupon list? 3. API endpoint: Path for validation — only if external API and no docs provided 4. HTTP method: GET, POST, PUT, or DELETE? — skip if API docs provided 5. Request params: Parameters or request body — skip if API docs provided 6. Response format: JSON structure of the API response — skip if API docs provided 7. Discount limits: No limit, Maximum percentage, Maximum value, or Depends on seller level? 8. Approval needed: No, Yes above a certain value, or Always?
Recommended extension points: cart.order-summary.after Required hooks: useCart, useCurrentUser, useExtension Template: API (IO Proxy or Direct Auth)
User Profile Display
1. Information to display: Name, Email, Sales metrics, Goals, Quick settings 2. Metrics source (if metrics selected): No metrics, Internal API, VTEX OMS, or Static data?
Recommended extension points: menu.drawer-content Required hooks: useCurrentUser, useExtension Template: Hook (no API if only using useCurrentUser data)
API Authentication Decision Tree
When the use case requires an external API:
Step 1: Check for API documentation and confirm API details
First — Ask: does the user have API documentation they can share?
- Yes → Go to the API Documentation Ingestion section. Extract HTTP method, request parameters/body, and response JSON structure from the docs. Present the extracted details to the user for confirmation, then proceed to Step 2.
- No → Collect manually: HTTP method, request parameters/body, and API response JSON structure. Proceed to Step 2.
Step 2: Does the API need authentication?
- No → Use the basic API template. Done.
- Yes → Continue to Step 3.
Step 3: Does the user have a VTEX IO proxy app?
A VTEX IO proxy app acts as a middleware: the Sales App extension calls the IO app using credentials: 'include' to forward session cookies, and the IO app calls the external API with the secret keys on the server side. The keys never leave the server.
- Yes → Collect the IO app endpoint path (relative, starting with
/). Use IO Proxy template. Done. - No → Continue to Step 4.
Step 4: Implement IO proxy first, or continue with insecure direct auth?
- Implement IO proxy first → Stop the Sales App extension workflow. The user should build the IO proxy app first and return later.
- Continue with insecure direct auth → Continue to Step 5.
Step 5: Security warning and direct auth details
⚠️ WARNING: Passing authentication keys directly in frontend code is NOT secure. The keys will be visible to anyone inspecting the browser. Confirm the user understands and accepts the risk.
If confirmed, collect:
- Auth header name (e.g.,
x-api-key,Authorization) - Auth header value (e.g.,
Bearer your-token,your-api-key) - Full API endpoint URL
Use the Direct Auth template.
IO Proxy Critical Rule
The fetch call in the extension must use ONLY the relative path (e.g., /_v/my-api/endpoint). NEVER prefix with https:// or a domain like {account}.myvtex.com. The Sales App has an internal proxy that automatically resolves the domain.
API Documentation Ingestion
When the user provides API documentation, follow these steps to extract structured information before generating code.
Supported input formats
| Format | How to load | Notes |
|---|---|---|
| URL | Use fetch_webpage to retrieve the page content | Works for REST API reference pages, Swagger UI, Redoc |
| OpenAPI/Swagger JSON | Parse the JSON directly from user input or file content | Look for paths, components/schemas keys |
| OpenAPI/Swagger YAML | Convert YAML mentally to JSON structure, then parse same as above | |
| Markdown doc | Read sections for endpoint paths, method, headers, request/response examples | |
| Inline text | Read the pasted content, identify patterns: GET /path, POST /path, JSON examples |
Extraction checklist
From any documentation format, extract and record:
1. Base URL — the host/prefix (e.g., https://api.example.com/v2). For IO Proxy, this becomes irrelevant — only the path matters. 2. Endpoints — for each endpoint: HTTP method, path, and purpose description. 3. Required headers — Content-Type, Authorization, x-api-key, or others. 4. Query parameters — name, type, required/optional. 5. Request body shape — JSON structure with field names, types, and required/optional status. 6. Response shape — JSON structure. If multiple status codes, capture the success response (200/201). 7. Error shapes — if documented, capture 4xx/5xx response structures.
Extraction output (mental model)
After extracting, summarize to the user in this format before proceeding to code generation:
Extracted API summary:
- Endpoint: [METHOD] [path]
- Auth: [None / IO Proxy at /_v/... / Direct with header X]
- Request: { field: type, ... }
- Response: { field: type, ... }
- Optional fields: [list]
Does this match what you expected?Wait for user confirmation before proceeding to Step 2 (template selection) or code generation.
Multiple endpoints
If the documentation describes multiple endpoints needed by the extension:
- List all of them in the summary above
- Note whether they can be called in parallel or must be sequential
- This will determine whether a custom fetch hook is needed (see code templates reference)
Extension Documentation Template
Generate docs/<ExtensionName>.md inside the Sales App package. Create the docs/ folder if it does not exist.
The document must contain these 9 sections:
1. Extension name — title matching the component name. 2. Overview — one-paragraph summary of what the extension does and why it was built. 3. Extension point — which extension point it registers on (e.g., cart.cart-list.after) and why that point was chosen. 4. Hooks used — list each hook (useCart, usePDP, etc.) with a brief explanation of what data it provides to this extension. 5. Component structure — description of the component tree, props, and state management. 6. Styling — CSS file name and summary of key classes. 7. API integration (if applicable) — endpoint, auth strategy (IO Proxy or direct), request/response shape.
- Source documentation: URL or file path of the original API documentation, if provided.
- Generated types: list of TypeScript interfaces generated from the API documentation.
- Note which fields are optional (
?) vs required per the documentation.
8. How to test — dev server command and URL to reach the extension. 9. Known constraints — any guards, edge cases, or limitations (e.g., useCartItem().item may be undefined).
# <ExtensionName>
## Overview
<One-paragraph description of the extension purpose and value.>
## Extension point
- **Point:** `<extension.point.name>`
- **Rationale:** <Why this extension point was selected.>
## Hooks used
| Hook | Purpose |
|------|---------|
| `useCart` | <What data it provides here> |
## Component structure
<Describe the component tree, key props, and internal state.>
## Styling
- **File:** `<ComponentName>.css`
- <Summary of key CSS classes and design decisions.>
## API integration
- **Endpoint:** `/_v/...`
- **Auth strategy:** IO Proxy / Direct / None
- **Request/Response:** <Brief shape description.>
## How to test
Run `yarn fsp dev {account}` and navigate to `https://{account}.myvtex.com/sales-app/...` to verify the extension renders.
## Known constraints
- <Guard or limitation 1>
- <Guard or limitation 2>Extension Points, Hooks, and Types
Extension Points
Sales App has 8 extension points across 3 categories.
Cart Extension Points
cart.cart-list.after
- Location: Below the cart items list
- Layout Shift: No
- Available Hooks: useCart, useExtension
- Use Cases: Loyalty points summary, promotional banners, gift options, shipping estimates
import { defineExtensions } from '@vtex/sales-app';
import { MyExtension } from './components/MyExtension';
export default defineExtensions({
'cart.cart-list.after': MyExtension,
});cart.cart-item.after
- Location: Below each individual cart item
- Layout Shift: Yes — use loading states and skeletons
- Available Hooks: useCart, useCartItem, useExtension
- Use Cases: Warranty options, item-specific promotions, personalization, service attachments
export default defineExtensions({
'cart.cart-item.after': ItemWarranty,
});cart.order-summary.after
- Location: Below the order summary/totals
- Layout Shift: Yes — use loading states and skeletons
- Available Hooks: useCart, useExtension
- Use Cases: Loyalty points redemption, additional fees/discounts, coupon input, installment options
export default defineExtensions({
'cart.order-summary.after': LoyaltyPoints,
});PDP Extension Points
pdp.sidebar.before
- Location: Above the sidebar content on the Product Detail Page
- Layout Shift: Yes
- Available Hooks: usePDP, useCart, useExtension
- Use Cases: Product badges, stock alerts, promotional banners, seller information
pdp.sidebar.after
- Location: Below the sidebar content on the Product Detail Page
- Layout Shift: Yes
- Available Hooks: usePDP, useCart, useExtension
- Use Cases: Warranty selection, related services, financing options, gift wrapping
pdp.content.after
- Location: Below the main content area on the Product Detail Page
- Layout Shift: Yes
- Available Hooks: usePDP, useCart, useExtension
- Use Cases: Product recommendations, customer reviews, specifications, complementary products
Menu Extension Points
menu.item
- Location: Sales App main menu
- Layout Shift: No
- Available Hooks: useExtension
- Use Cases: Custom navigation links, quick action buttons, menu notifications
menu.drawer-content
- Location: Sales App menu drawer
- Layout Shift: No
- Available Hooks: useCurrentUser, useExtension
- Use Cases: User profile information, sales performance metrics, settings shortcuts, notifications
Hooks Reference
All hooks are imported from @vtex/sales-app.
useCart
Access cart data and perform mutations.
import { useCart } from '@vtex/sales-app';
const cart = useCart();Available in: cart.cart-list.after, cart.cart-item.after, cart.order-summary.after, pdp.sidebar.before, pdp.sidebar.after, pdp.content.after
Returns:
| Property | Type | Description |
|---|---|---|
orderFormId | `string \ | undefined` |
value | number | Total cart value in cents |
items | CartItem[] | Array of items in the cart |
totalizers | Totalizers[] | Array of totalizers (Items, Shipping, Discounts, Tax) |
clientProfileData | ClientProfileData | Client profile (email, document, phone) |
giftCards | GiftCard[] | Gift cards currently attached to the cart |
addItem | (data: UseCartAddItemData) => Promise<void> | Add an item to the cart |
removeItem | (id: string, index: number) => Promise<void> | Remove an item from the cart |
addCoupon | (coupon: string) => Promise<void> | Add a coupon to the cart |
addGiftCard | `(redemptionCodeOrGiftCard: string \ | GiftCard, provider?: string) => Promise<void>` |
sync | () => Promise<void> | Sync the cart with the latest Order Form data |
Example — display total items:
const TotalItems = () => {
const cart = useCart();
return <div>Items: {cart.items.length}</div>;
};Example — add item to cart:
const AddToCart = () => {
const cart = useCart();
const addItem = () => cart.addItem({
quantity: 1,
seller: '1',
id: '8392'
});
return <button onClick={addItem}>Add to cart</button>;
};Example — add gift card:
const AddGiftCard = () => {
const cart = useCart();
const add = async () => {
await cart.addGiftCard('ABC-123', 'my-giftcard-provider');
await cart.sync();
};
return <button onClick={add}>Add gift card</button>;
};useCartItem
Access individual cart item data. Only available in `cart.cart-item.after`.
import { useCartItem } from '@vtex/sales-app';
const { item, itemIndex, changeItem, changePrice } = useCartItem();
// IMPORTANT: item may be undefined — always check first
if (!item) return (<></>);Returns:
| Property | Type | Description |
|---|---|---|
item | `CartItem \ | undefined` |
itemIndex | `number \ | undefined` |
changeItem | (data: UseCartItemChangeItemData) => Promise<void> | Modify quantity or attachments |
changePrice | (price: number) => Promise<void> | Change item price (requires allowManualPrice in orderForm) |
useCurrentUser
Access current authenticated user data. Only available in `menu.drawer-content`.
import { useCurrentUser } from '@vtex/sales-app';
const { name, email } = useCurrentUser();Returns:
| Property | Type | Description |
|---|---|---|
name | string | Current user's name |
email | string | Current user's email |
useExtension
Access account and extension point context. Available in all extension points.
import { useExtension } from '@vtex/sales-app';
const { account, extensionPoint } = useExtension();Returns:
| Property | Type | Description |
|---|---|---|
account | string | Current VTEX account name |
extensionPoint | ExtensionPointsType | Name of the extension point where the component is mounted |
usePDP
Access Product Detail Page data. Only available in PDP extensions.
import { usePDP } from '@vtex/sales-app';
const { productSku } = usePDP();Returns:
| Property | Type | Description |
|---|---|---|
productSku | ProductSku | Current product SKU data |
TypeScript Types
type CartItem = {
id: string;
name: string;
quantity: number;
seller: string;
sellingPrice: number;
listPrice: number;
manualPrice?: number; // optional — guard before use
price: number;
imageUrl: string;
productRefId?: string; // optional — guard before use
attachments?: Attachment[]; // optional — guard before use
};
type ClientProfileData = {
email: string | null;
document: string | null;
phone: string | null;
};
type ProductSku = {
id: string;
name: string;
quantity: number;
price: number;
listPrice: number;
sellingPrice?: number;
};
type Totalizers = {
id: string; // Common IDs: "Items", "Shipping", "Discounts", "Tax"
name: string;
value: number; // in cents
};
type Attachment = {
name: string;
content: Record<string, string>;
};
type GiftCard = {
id: string;
redemptionCode?: string | null;
name: string;
caption: string;
value: number;
balance: number;
provider: string;
groupName?: string | null;
inUse: boolean;
isSpecialCard: boolean;
};
type UseCartAddItemData = {
id: string;
quantity: number;
seller: string;
attachments?: Attachment[];
};
type UseCartItemChangeItemData = {
quantity?: number;
attachments?: Attachment[];
};
type ExtensionPointsType =
| 'cart.cart-list.after'
| 'cart.cart-item.after'
| 'cart.order-summary.after'
| 'pdp.sidebar.before'
| 'pdp.sidebar.after'
| 'pdp.content.after'
| 'menu.item'
| 'menu.drawer-content';Optional Properties Warning
Three CartItem properties are optional and must be guarded:
| Property | Type | Safe Access |
|---|---|---|
manualPrice | `number \ | undefined` |
productRefId | `string \ | undefined` |
attachments | `Attachment[] \ | undefined` |
Local Development, Build, and Deployment
Local Development (Step 4)
Start the dev server
# Verify dependencies are installed
yarn install
# Primary command — run at monorepo root
yarn fsp dev {account_name}
# Alternative with more control
yarn sales-app dev {account_name} ./{account_name}/sales-app 3002
# Show extension point placeholders (useful for finding where to render)
yarn sales-app dev {account_name} ./{account_name}/sales-app 3002 --show-placeholdersTest URLs
| Page | URL |
|---|---|
| Cart | http://localhost:3002/sales-app/checkout/cart |
| PDP | http://localhost:3002/sales-app/product/<product-id> |
| Base | http://localhost:3002/sales-app |
Development tips
- Hot reload is active — code changes reflect automatically
- Use
--show-placeholdersto see where extension points are located - Check the port configuration in
faststore.jsonif there are conflicts
Troubleshooting
| Issue | Solution |
|---|---|
| Port in use | Try a different port: 3003, 3004 |
| Extension not visible | Check index.tsx and the configured extension point name |
| Build error during dev | Run yarn fsp build {account_name} sales-app for detailed errors |
| Module not found | Run yarn install to ensure dependencies are linked |
Build (Step 5a)
Build command
yarn fsp build {account_name} sales-appWhat the build checks
1. TypeScript — types and syntax errors 2. Imports — referenced modules and files exist 3. JSX — correct React component syntax 4. CSS — style files imported correctly 5. defineExtensions — valid extension points configuration
Common build errors
| Error | Cause | Solution |
|---|---|---|
| TypeScript errors | Type mismatches, missing types | Check variable types and @vtex/sales-app imports |
| Module not found | Wrong import path or missing file | Verify file exists at the imported path |
| Hook errors | Hooks inside conditionals or loops | Move hook calls to the top level of the component |
| CSS errors | Missing or misnamed CSS file | Check .css file exists and import path is correct |
| defineExtensions errors | Invalid extension point or missing export | Verify extension point name and component export |
Build tip
During development, yarn fsp dev already runs the build automatically. Use yarn fsp build when you want to validate before deploying.
Deployment (Step 5b)
Deployment is automatic via FastStore WebOps when you push to the main branch.
Deployment steps
# 1. Verify local build passes
yarn fsp build {account_name} sales-app
# 2. Review changes
git status
# 3. Stage files
git add .
# 4. Create commit
git commit -m "feat: add Sales App extension"
# 5. Push to main
git push origin mainAfter pushing:
6. Monitor deploy — Access FastStore WebOps in VTEX Admin (https://{account_name}.myvtex.com/admin/faststore-webops) 7. Wait for propagation — Deployment takes 5-10 minutes (includes CDN cache propagation) 8. Validate in production — Access Sales App in production and verify the extension
Monitoring
FastStore WebOps:
- URL: VTEX Admin > FastStore > WebOps
- Shows build and deployment status
GitHub Checks:
- Go to the GitHub repository
- See the status icon (✔️, ❌, 🟡) next to each commit
Rollback
If something goes wrong after deployment:
git revert HEAD
git push origin mainTroubleshooting deployment
Build failed in WebOps
1. Go to the GitHub repository 2. Click the status icon next to the commit 3. Select "Details" under "FastStore WebOps" 4. Read the error logs 5. Fix the issue locally 6. Make a new commit and push
Extension doesn't appear after deploy
1. Check the extension point is correct in index.tsx 2. Clear browser cache (Ctrl+Shift+R) 3. Wait longer for propagation (up to 10 minutes) 4. Verify you're accessing the correct account
Runtime error in production
1. Open the browser console (F12) 2. Check for JavaScript errors 3. If API error, verify URLs and authentication 4. Fix locally, test, and redeploy
Static Analysis Rules (fsp-analyzer)
Sales App extensions run inside a sandboxed environment enforced by @vtex/fsp-analyzer. These rules are checked at preBuild and preDev hook time. The AI must validate all generated code against these rules before presenting it to the user.
Rule IDs match the ViolationKind, ReactViolationKind, WarningKind, and ReactWarningKind types from @vtex/fsp-analyzer so developers can cross-reference with real build output.
---
How to apply these rules
After generating a component, CSS file, and index.tsx, check each generated file against the violation rules (block generation — fix before presenting) and the warning rules (flag to the user with a suggestion). Do not present code that has unresolved violations.
---
1. Sandbox Security Rules
Applied to .ts and .tsx files by FastStoreSandboxAnalyzer.
RESTRICTED_API — DOM API access banned
Severity: Violation (block) Files: .ts, .tsx
Restricted identifiers: document, window, localStorage, sessionStorage, navigator.
Detection: Any use of these identifiers as a value (not as a type import).
Correct
// Use React state and props for data; use useExtension() for account context
const { account } = useExtension();Wrong
const userId = localStorage.getItem('userId');
const width = window.innerWidth;
document.title = 'My Extension';---
VARIABLE_ALIASING — Aliasing restricted APIs
Severity: Violation (block) Files: .ts, .tsx
Assigning a restricted API to a variable to circumvent detection.
Detection: const x = document, const [w, d] = [window, document], or any destructuring that aliases a restricted API.
Correct
// Pass data via props or React context, not via aliased globalsWrong
const doc = document;
const [w, d] = [window, document];
doc.querySelector('.item');---
GLOBAL_MANIPULATION — Global object manipulation
Severity: Violation (block) Files: .ts, .tsx
Restricted member expressions: window.location, window.history.
Detection: Member expressions on window to access location or history.
Correct
// Navigation is managed by Sales App shell; do not manipulate locationWrong
window.location.href = '/new-path';
window.location.reload();---
HISTORY_MANIPULATION — Browser history manipulation
Severity: Violation (block) Files: .ts, .tsx
Restricted calls: window.history.pushState, window.history.replaceState, history.pushState, history.replaceState.
Detection: Any call to these methods.
Correct
// Let the Sales App shell handle routingWrong
history.pushState({}, '', '/cart');
window.history.replaceState(null, '', '/pdp');---
DYNAMIC_SCRIPT_CREATION — Dynamic script creation
Severity: Violation (block) Files: .ts, .tsx
Restricted expressions: document.createElement, document.write, document.writeln, document.body.appendChild, document.head.appendChild.
Detection: Any call to these methods.
Correct
// Render UI via JSX; never inject scripts dynamicallyWrong
const script = document.createElement('script');
script.src = 'https://cdn.example.com/lib.js';
document.body.appendChild(script);---
CODE_EXECUTION — Arbitrary code execution
Severity: Violation (block) Files: .ts, .tsx
Restricted calls: eval, Function (as constructor), setTimeout (string form), setInterval (string form), new Function.
Detection: Any call or instantiation of these identifiers.
Note on `setTimeout`/`setInterval`: These are restricted in the sandbox to prevent arbitrary code injection. UseuseEffectwithAbortControlleror cleanup functions for timing-based logic:
Correct
useEffect(() => {
const id = window.setTimeout(() => { /* logic */ }, 500);
return () => window.clearTimeout(id);
}, []);
// Note: even this should be reviewed; prefer event-driven patternsWrong
eval('doSomething()');
const fn = new Function('return 42');
setTimeout('doSomething()', 1000);---
RESTRICTED_IMPORT — Restricted Node.js module imports
Severity: Violation (block) Files: .ts, .tsx
Restricted modules: fs, path, child_process, crypto.
Detection: Any import statement importing from these module names.
Correct
// Extensions are browser-only React components; do not import Node.js modules
import { useCart } from '@vtex/sales-app';Wrong
import fs from 'fs';
import { resolve } from 'path';
import crypto from 'crypto';---
RESTRICTED_IMPORT_SOURCE — Direct node_modules path imports
Severity: Violation (block) Files: .ts, .tsx
Detection: Import paths containing node_modules/.
Wrong
import something from 'node_modules/lodash/get';---
NON_APPROVED_PACKAGE — Import from non-approved package
Severity: Warning (flag) Files: .ts, .tsx
Any import whose source starts with @ or contains /, unless it is react or a node_modules/ direct path. Fires for every @vtex/* scoped package and every relative import (./foo, ../bar).
Note: This warning is noisy — it fires for all legitimate Sales App imports (@vtex/sales-app,@phosphor-icons/react, relative component paths). Do not surface to the user when the import is from@vtex/*or a relative path. Investigate only when the import is from an unexpected third-party package.
Detection: import { … } from 'source' where source.startsWith('@') or source.includes('/'), and source is not 'react' or a node_modules/ path.
---
EXTERNAL_RESOURCE_LOADING — Unrestricted external resource loading
Severity: Violation (block) Files: .ts, .tsx
Two detection paths:
1. Direct API use: fetch, XMLHttpRequest, script.src, link.href used as member expressions. 2. Known CDN domains in string literal: fetch('https://cdn.jsdelivr.net/...') or any URL containing cdn.jsdelivr.net, unpkg.com, cdnjs.cloudflare.com, ajax.googleapis.com, code.jquery.com, api.example.com, or ending with .js.
Exception:fetchvia the IO Proxy relative path (/_v/...) is allowed and required for secure API integration.
Correct
const response = await fetch('/_v/my-loyalty-api/points', {
credentials: 'include',
});Wrong
const response = await fetch('https://api.example.com/data');
const response = await fetch('https://cdn.jsdelivr.net/npm/lodash.js'); // CDN — also Violation
const xhr = new XMLHttpRequest();---
EXTERNAL_RESOURCE_FETCH — Fetch to known CDN domains
Note: This rule ID is declared in the analyzer type system but is never raised by any handler. CDN domain fetches are actually reported as EXTERNAL_RESOURCE_LOADING (Violation, block) — not as this Warning. Treat any fetch to a CDN domain as a build-blocking Violation, not a soft warning.Severity: Warning (flag) — see note above; actual enforcement is via `EXTERNAL_RESOURCE_LOADING` Files: .ts, .tsx
Known third-party domains: cdn.jsdelivr.net, unpkg.com, cdnjs.cloudflare.com, ajax.googleapis.com, code.jquery.com.
Detection: fetch() first argument literal containing one of these domains.
---
QUERY_SELECTOR_USAGE — DOM query selectors
Severity: Violation (block) Files: .ts, .tsx
Restricted calls: document.getElementById, document.getElementsByClassName, document.getElementsByTagName, document.querySelector, document.querySelectorAll, and element-scoped variants.
Detection: Any call where the callee matches document.querySelector, document.getElementById, etc. (the variable must be literally named document). Calls via a ref object — e.g. containerRef.current.querySelector(...) — are not caught by the analyzer and must be avoided as a Hard Constraint.
Correct
// Use React refs for direct DOM access when absolutely needed
const ref = useRef<HTMLDivElement>(null);Wrong
const el = document.querySelector('.my-class');
document.getElementById('cart-container');---
STYLE_MANIPULATION — Direct style manipulation
Severity: Violation (block) Files: .ts, .tsx
Restricted expressions: element.style, element.className, element.classList, element.setAttribute.style, element.setAttribute.class, element.hidden, element.display, element.visibility.
Detection: Matches only when the object is literally named element (e.g. element.style). Equivalent access through other variable names — e.g. divRef.current.style, el.className — is not caught by the analyzer and must be avoided as a Hard Constraint.
Correct
// Use plain CSS classes from the component stylesheet
import './MyExtension.css';
return <div className={isActive ? 'active' : 'inactive'} />;Wrong
element.style.color = 'red';
element.classList.add('active');
element.setAttribute('style', 'display: none');---
INFINITE_LOOP — Infinite loops
Severity: Violation (block) Files: .ts, .tsx
Detection: while(true) or do...while(true) with literal true condition.
Wrong
while (true) {
processItem();
}---
POTENTIAL_INFINITE_LOOP — Incomplete for-loop
Severity: Warning (flag) Files: .ts, .tsx
Detection: for statement with no update expression AND no test expression (e.g. for (let i = 0;;) { … }).
Action: Warn the user and ensure the loop has a proper termination condition.
---
MEMORY_LEAK — Memory leak patterns
Note: This rule ID is declared in the analyzer type system but is never raised by any handler. The active rule isPOTENTIAL_MEMORY_LEAK(Warning) below. Event listeners without cleanup will not block the build, but remain a Hard Constraint — always add a cleanup return inuseEffect.
Severity: Violation (block) — see note above; not currently enforced at build time Files: .ts, .tsx
Detection: Event listeners, subscriptions, or timers added without cleanup in useEffect.
Correct
useEffect(() => {
const handler = () => { /* ... */ };
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);Wrong
useEffect(() => {
window.addEventListener('resize', handler);
// Missing cleanup — memory leak
}, []);---
POTENTIAL_MEMORY_LEAK — Likely memory leak patterns
Severity: Warning (flag) Files: .ts, .tsx
Three triggers: 1. Any addEventListener call — the analyzer warns regardless of whether a corresponding removeEventListener exists in cleanup. 2. Recursive function call where the callee name matches the parent FunctionDeclaration identifier. 3. Array.push inside a for or while loop.
Action: Verify the component has a proper cleanup return in useEffect for any event listener, and that recursive functions have a clear base case.
Correct
useEffect(() => {
const handler = () => { /* ... */ };
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);---
EXCESSIVE_API_CALLS — Excessive API call patterns
Severity: Violation (block) Files: .ts, .tsx
Detection: fetch or XMLHttpRequest calls whose parent is a for or while loop.
Wrong
// fetch inside loop — excessive calls
for (const item of items) {
await fetch(`/_v/api/item/${item.id}`);
}---
FREQUENT_API_CALLS — API call outside loop / rapid setInterval
Severity: Warning (flag) Files: .ts, .tsx
Two triggers: 1. fetch or XMLHttpRequest call outside a loop (i.e. anywhere else in the component). Fires for every fetch — ensure calls are wrapped in useEffect with proper deps and are rate-limited or cached. 2. setInterval with a numeric delay argument below 1000 ms.
Note: Trigger 1 is intentionally broad — it flags everyfetch()call as a reminder to verify it is not running unintentionally on every render. It does not mean every fetch is wrong. Only surface this to the user when the fetch is genuinely outside auseEffector appears to run on each render.
Action: Warn the user and suggest wrapping fetches in useEffect, using caching, or increasing the polling interval.
---
LARGE_BUNDLE_SIZE_IMPACT — Bundle size impacts
Severity: Warning (flag) Files: .ts, .tsx
Detection:
- String literals longer than 1,000 characters
- Object literals with more than 100 properties
- Imports from heavyweight libraries:
lodash,moment
Action: Warn the user and suggest alternatives (date-fns instead of moment, native array methods instead of lodash).
Correct
import { format } from 'date-fns';
const formattedDate = format(new Date(), 'yyyy-MM-dd');Wrong
import moment from 'moment';
import _ from 'lodash';---
2. CSS Containment Rules
Applied to .css, .scss, .less files by CSSAnalyzer.
Sales App analyzer configuration (from @vtex/sales-app build/dev hook):
cssOptions: {
allowedNamespaces: ['sales-app-extension-'],
defaultNamespace: 'sales-app-extension-',
transformNonCompliant: true,
overwriteTransformed: true,
}Two consequences for the rules below:
1. The configured namespace is `sales-app-extension-`, not the analyzer's bare default of extension-. Any rule that talks about "the allowed namespace" means sales-app-extension- in this project. 2. transformNonCompliant: true softens the namespace rules: when a selector or @keyframes name is not prefixed, the analyzer auto-rewrites it into a sibling *.transformed.css file and emits a CSS_TRANSFORMED warning instead of a CSS_NAMESPACE_REQUIRED / CSS_GLOBAL_KEYFRAMES violation. The original .css (and the className strings in JSX) are left untouched, so the build does not break. This is why the CSS template in code-templates-and-patterns.md ships unprefixed selectors like .container, .title, .row — they are valid in this project.
The other CSS rules (CSS_GLOBAL_SELECTOR, CSS_CONTAINMENT_BREAKOUT, CSS_GLOBAL_IMPORT, CSS_RESTRICTED_PROPERTY, CSS_RESTRICTED_VALUE, and the inline-style rules) are not softened by transformNonCompliant — they remain build-blocking violations.
CSS_GLOBAL_SELECTOR — Global element selectors
Severity: Violation (block) Files: .css
Restricted selectors: *, body, html, :root, head, main, #root, #__next.
Detection: Any CSS rule whose selector matches or contains these elements at the top level.
Correct
/* Scope rules inside the extension container — never target global elements */
.container {
font-size: 14px;
}
.container .title {
font-weight: bold;
}Wrong
body {
font-size: 14px;
}
* {
box-sizing: border-box;
}
:root {
--my-color: red;
}---
CSS_CONTAINMENT_BREAKOUT — CSS containment breakout
Severity: Violation (block) Files: .css
Restricted patterns: :host, :host-context, ::slotted, :global, position: fixed, position: absolute, z-index: 9999.
Detection: Any rule or declaration matching these patterns.
Note on positioning:position: relativeandposition: stickywithin the extension container are generally safe. Avoidfixedand unboundedabsolutethat escape the extension boundary.
Correct
.container {
position: relative; /* safe — contained */
}Wrong
.overlay {
position: fixed;
z-index: 9999;
top: 0;
left: 0;
}
:host {
display: block;
}---
CSS_NAMESPACE_REQUIRED — Missing CSS namespace
Severity: Warning in this project (auto-transformed) — would be Violation if transformNonCompliant were false Files: .css
All CSS selectors should be scoped under an allowed namespace prefix. In the Sales App, that prefix is sales-app-extension-. Because the project sets transformNonCompliant: true, an unprefixed selector does not block the build: the analyzer rewrites it inside a sibling Component.transformed.css and emits a CSS_TRANSFORMED warning instead of CSS_NAMESPACE_REQUIRED. The original Component.css (which is what gets bundled) keeps the unprefixed class, so the JSX className="container" continues to match.
Detection: A selector that does not include any value from allowedNamespaces (substring check on .namespace, #namespace, or [class*="namespace"]).
Correct (and matches the CSS template)
/* Unprefixed top-level classes are accepted because transformNonCompliant=true. */
.container { padding: 8px; }
.title { font-size: 16px; }Also correct (explicit prefix, never warns)
.sales-app-extension-container { padding: 8px; }
.sales-app-extension-title { font-size: 16px; }Prefer the unprefixed form to match the CSS template in code-templates-and-patterns.md and the JSX in the component templates. The CSS_TRANSFORMED warning that fires for these is informational only.---
CSS_GLOBAL_IMPORT — CSS @import rules
Severity: Violation (block) Files: .css
@import pulls external stylesheets that affect global styles and bypass containment.
Detection: Any @import at-rule anywhere in the CSS file.
Correct
/* Import a local CSS variable file via bundler config, not @import */Wrong
@import url('https://fonts.googleapis.com/css2?family=Roboto');
@import './reset.css';---
CSS_GLOBAL_KEYFRAMES — Keyframes without namespace
Severity: Warning in this project (auto-transformed) — would be Violation if transformNonCompliant were false Files: .css
@keyframes names are global. Without a namespace they may conflict with other extensions or the Sales App shell. As with CSS_NAMESPACE_REQUIRED, transformNonCompliant: true rewrites the name in *.transformed.css and emits CSS_TRANSFORMED instead of blocking.
Detection: @keyframes whose name does not start with an allowed namespace prefix (sales-app-extension- in this project).
The CSS template prefixes keyframes with ${COMPONENT_NAME}- (e.g. LoyaltyPoints-spin). That prefix does not start with sales-app-extension-, so it triggers a CSS_TRANSFORMED warning — the build still succeeds, but the transformed file will rename it to sales-app-extension-LoyaltyPoints-spin. Either form is acceptable.
Correct (no warning)
@keyframes sales-app-extension-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}Also correct (CSS_TRANSFORMED warning, build passes)
@keyframes LoyaltyPoints-spin {
to { transform: rotate(360deg); }
}---
CSS_RESTRICTED_PROPERTY — Restricted CSS properties
Severity: Violation (block) Files: .css
Certain properties affect layout or rendering globally and are restricted to prevent layout shift or containment breakout.
Detection: Declarations using restricted property names (e.g., properties that set global layout hints).
Action: If detected, remove or scope the declaration appropriately.
---
CSS_RESTRICTED_VALUE — Restricted CSS values
Severity: Violation (block) Files: .css
Certain CSS values (e.g., !important) override the cascade and break containment guarantees.
Detection: !important in any declaration, or values targeting global layout tokens.
Correct
.button {
color: var(--sa-color-primary);
}Wrong
.button {
color: red !important;
}---
CSS_RESTRICTED_INLINE_PROPERTY — Restricted property in inline style
Severity: Violation (block) Files: .tsx
Inline style={{ … }} props in JSX may not use properties that affect layout or containment.
Restricted properties: position, z-index, top, left, right, bottom, all, contain, content, isolation.
Detection: A JSX style prop object containing one of the restricted property names.
Correct
// Use CSS classes for layout properties
return <div className="overlay" />;Wrong
return <div style={{ position: 'fixed', top: 0 }} />;---
CSS_RESTRICTED_INLINE_VALUE — Restricted value in inline style
Severity: Violation (block) Files: .tsx
Inline style={{ … }} props may not use values that break containment or cascade.
Restricted values: fixed, absolute, !important, inherit, initial, unset.
Detection: A JSX style prop object containing one of the restricted values.
Wrong
return <div style={{ position: 'fixed' }} />;
return <div style={{ display: 'inherit' }} />;---
3. React Performance Rules
Applied to .tsx files by ReactPerformanceAnalyzer. Violations block presentation; warnings are flagged to the user.
REACT_UNNECESSARY_RERENDER — State update inside loop
Severity: Violation (block) Files: .tsx
State setter calls inside for or while loops trigger a re-render on every iteration.
Detection: A setState / set* call whose parent node is a for or while statement.
Correct
// Compute derived value, then set once
const updatedItems = items.map(transform);
setItems(updatedItems);Wrong
for (const item of items) {
setCount((c) => c + 1); // re-renders on every iteration
}---
REACT_ANONYMOUS_COMPONENT — Anonymous (lowercase) component
Severity: Violation (block) Files: .tsx
Components assigned to variables starting with lowercase are treated as HTML tags by React and cannot be used in JSX.
Detection: Arrow function or function expression assigned to a variable that does not start with an uppercase letter.
Correct
export function LoyaltyPoints(): JSX.Element { ... }
export const WarrantyBadge = (): JSX.Element => { ... };Wrong
const loyaltyPoints = (): JSX.Element => { ... };
const warrantyBadge = function() { ... };---
REACT_DIRECT_DOM_MANIPULATION — Direct DOM manipulation in React
Severity: Violation (block) Files: .tsx
Bypasses the React virtual DOM and causes unpredictable behavior.
Detection: Calls to document.querySelector or document.getElementById inside a React component.
Correct
const containerRef = useRef<HTMLDivElement>(null);
// Access DOM via containerRef.currentWrong
const el = document.querySelector('.cart-list');
el.style.display = 'none'; // bypasses React---
REACT_LAYOUT_EFFECT_MISUSE — useLayoutEffect usage
Severity: Violation (block) Files: .tsx
useLayoutEffect blocks the browser paint and can cause performance issues in a sandboxed extension.
Detection: Any call to useLayoutEffect.
Correct
useEffect(() => {
// DOM reading/writing after paint
}, []);Wrong
useLayoutEffect(() => {
// blocks paint
}, []);---
REACT_MISSING_DEPS — Missing dependency array
Severity: Warning (flag) Files: .tsx
useEffect, useCallback, or useMemo without a second argument re-runs on every render.
Detection: A call to one of these hooks with fewer than 2 arguments, or with a non-array second argument.
Correct
useEffect(() => {
fetchData();
}, [productId]); // explicit depsWrong
useEffect(() => {
fetchData(); // runs on every render
});---
REACT_INLINE_FUNCTION — Inline functions in JSX props
Severity: Warning (flag) Files: .tsx
Arrow functions defined directly in JSX attributes create a new function reference on every render, preventing child memoization.
Detection: Function expression or arrow function expression whose parent is a JSX expression container or prop.
Correct
const handleClick = useCallback(() => {
doSomething();
}, []);
return <button onClick={handleClick}>Click</button>;Wrong
return <button onClick={() => doSomething()}>Click</button>;---
REACT_LARGE_COMPONENT — Component over 200 lines
Severity: Warning (flag) Files: .tsx
Large components are harder to maintain and test. Threshold: 200 lines.
Detection: A function/arrow function with start/end line difference > 200.
Action: Suggest splitting into smaller sub-components or extracting logic into custom hooks.
---
REACT_MANY_USESTATE — More than 5 useState calls
Severity: Warning (flag) Files: .tsx
Many useState calls in a single component indicate complex local state. Threshold: 5 calls.
Detection: More than 5 calls to useState (or React.useState) within a single function component.
Action: Suggest consolidating related state into a single useState with an object, or extracting to a custom hook.
Correct
const [formState, setFormState] = useState({
name: '',
email: '',
quantity: 1,
});Wrong
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [quantity, setQuantity] = useState(1);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [submitted, setSubmitted] = useState(false); // 6th useState — flagged---
REACT_CONDITIONAL_HOOK — Hook called conditionally
Severity: Warning (flag) Files: .tsx
Calling a hook inside an if, switch, or ternary expression violates the Rules of Hooks.
Detection: A hook call (useEffect, useState, useMemo, useCallback, useLayoutEffect) whose ancestor is an IfStatement, SwitchStatement, or ConditionalExpression.
Correct
const cart = useCart(); // always called unconditionally
if (!cart) return <></>;Wrong
if (isEnabled) {
const cart = useCart(); // conditional hook call — violates Rules of Hooks
}---
REACT_COMPLEX_JSX — Deep JSX nesting
Severity: Warning (flag) Files: .tsx
Deep JSX trees are harder to read and may indicate a need to extract sub-components. Threshold: 5 levels deep.
Detection: A JSX element whose JSX child tree exceeds 5 levels of nesting.
Action: Extract inner JSX into a named sub-component.
---
REACT_UNOPTIMIZED_LIST — List rendering without key or large list
Severity: Warning (flag) Files: .tsx
Missing key props in list rendering causes React reconciliation issues. Lists over 50 items may need virtualization.
Detection:
map()producing JSX without akeyattribute on the root element- List size exceeding 50 items
Correct
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);Wrong
return (
<ul>
{items.map((item) => (
<li>{item.name}</li> // missing key
))}
</ul>
);---
REACT_COMPLEX_STATE — useState with complex object
Severity: Warning (flag) Files: .tsx
useState initialized with an object having more than 5 properties suggests a need for useReducer. Threshold: 5 properties.
Detection: useState(obj) where obj is an object literal with > 5 properties.
Action: Suggest converting to useReducer for complex state.
---
REACT_NESTED_UPDATE — Nested state updates
Severity: Warning (flag) Files: .tsx
Calling a state setter from within another state setter's callback can cause unexpected re-renders.
Detection: A set* call whose ancestor is another set* call expression.
---
Validation Output Format
When reporting issues after code review, group by file and severity:
### Static Analysis Results
**Violations (must fix before completing Step 3):**
- [RULE_ID] File: ComponentName.tsx, Line ~N — <message>
Fix: <suggested fix>
**Warnings (review with user):**
- [RULE_ID] File: ComponentName.tsx, Line ~N — <message>
Suggestion: <improvement>
✅ No violations found. N warnings flagged.Do not proceed with Step 4 (Documentation) until all violations are resolved.