Menu
Guides
Storefront Development

Storefront Development

Overriding a native component

7 min read
This guide applies to stores using the CMS with FastStore versions 3 or 4. For stores using Headless CMS (legacy), see Overriding a native component.
FastStore includes native component sections such as ProductShelf, ProductDetails, and Breadcrumb. When a native component already meets most of your store's needs, you can override it to keep its existing data fetching and behavior while changing only the parts you need.
In this guide, you'll override the native ProductShelf component to display a Pix discount message on its product cards. You'll also make this message configurable in the CMS while keeping the native carousel unchanged.
{"base64":"  ","img":{"width":1293,"height":586,"type":"png","mime":"image/png","wUnits":"px","hUnits":"px","length":336879,"url":"https://vtexhelp.vtexassets.com/assets/docs/src/override-component-3___06a05f31d4a330b60b57d046ba75f47b.png"}}
If you need to create a section that doesn't have a native FastStore counterpart, see Creating a new section in the CMS.

Before you begin

Overriding a native component touches both your store code and the CMS, so you need a working CMS setup, the Content plugin installed locally, and a clear idea of which part of the section you want to change. Make sure the following is in place before you start:
  • The CMS must be installed and enabled in your VTEX account.
  • The Content plugin must be installed on your machine (vtex plugins install @vtex/cli-plugin-content) and up to date (vtex plugins update).
  • You know your CMS store ID. It's the contentSource.project value in your project's discovery.config.js.
  • Identify which native section and which overridable component you want to change. See the List of native sections and overridable components.

Instructions

Step 1 - Create the overridden section

  1. Open your store project in a code editor.
  2. In src/components/sections, create a ProductShelf folder with an index.tsx file.
  3. Import the native section and getOverriddenSection from @faststore/core. Since we're keeping the native carousel, we only override the product card overridable component (__experimentalProductCard), wrapping the native card rather than replacing it:
src/components/sections/ProductShelf/index.tsx

_80
import { useMemo, type ComponentProps } from 'react'
_80
import { getOverriddenSection, ProductShelfSection } from '@faststore/core'
_80
import NativeProductCard, {
_80
type ProductCardProps,
_80
} from 'src/components/product/ProductCard'
_80
_80
import styles from './PixDiscountBadge.module.scss'
_80
_80
type NativeProductShelfProps = ComponentProps<typeof ProductShelfSection>
_80
_80
type ProductShelfProps = Omit<
_80
NativeProductShelfProps,
_80
'productCardConfiguration'
_80
> & {
_80
productCardConfiguration?: NativeProductShelfProps['productCardConfiguration'] & {
_80
/**
_80
* Shows a "% off paying with Pix" message below the product card, computed
_80
* from the same discount used by the native discount badge.
_80
*/
_80
showPixDiscount?: boolean
_80
}
_80
}
_80
_80
// Keeps the native carousel untouched; only the product card overridable component is overridden.
_80
function withPixDiscount(showPixDiscount: boolean) {
_80
return function PixDiscountProductCard(props: ProductCardProps) {
_80
if (!showPixDiscount) {
_80
return <NativeProductCard {...props} />
_80
}
_80
_80
const {
_80
offers: {
_80
lowPrice,
_80
offers: [{ listPrice }],
_80
},
_80
} = props.product
_80
_80
const discountPercentage =
_80
listPrice > 0 ? Math.round(100 - (lowPrice / listPrice) * 100) : 0
_80
_80
return (
_80
<div data-fs-pix-discount-card>
_80
<NativeProductCard {...props} />
_80
{discountPercentage > 0 && (
_80
<span className={styles.pixDiscountBadge}>
_80
{discountPercentage}% off paying with Pix
_80
</span>
_80
)}
_80
</div>
_80
)
_80
}
_80
}
_80
_80
function ProductShelf(props: ProductShelfProps) {
_80
const { showPixDiscount = false, ...productCardConfiguration } =
_80
props.productCardConfiguration ?? {}
_80
_80
// Memoized so a new component identity isn't created on every render.
_80
const OverriddenProductShelf = useMemo(
_80
() =>
_80
getOverriddenSection({
_80
Section: ProductShelfSection,
_80
components: {
_80
__experimentalProductCard: {
_80
Component: withPixDiscount(showPixDiscount),
_80
},
_80
},
_80
}),
_80
[showPixDiscount]
_80
)
_80
_80
return (
_80
<OverriddenProductShelf
_80
{...props}
_80
productCardConfiguration={productCardConfiguration}
_80
/>
_80
)
_80
}
_80
_80
export default ProductShelf

src/components/sections/ProductShelf/PixDiscountBadge.module.scss

_10
.pixDiscountBadge {
_10
display: block;
_10
margin-top: var(--fs-spacing-tiny, 0.25rem);
_10
color: var(--fs-color-success-text, var(--fs-color-main-3));
_10
font-size: var(--fs-text-size-0);
_10
font-weight: var(--fs-text-weight-medium);
_10
}

Step 2 - Declare the CMS schema

Run vtex content init if you haven't already. It prompts for a store ID (the default shown is faststore; type your actual CMS store ID instead, matching contentSource.project in discovery.config.js) and scaffolds:

_10
cms/{storeId}/
_10
├── components/
_10
│ └── cms_component__bannerExample.jsonc.example
_10
└── pages/
_10
└── cms_content_type__landingPage.jsonc.example

These .jsonc.example files are placeholder templates, not live schemas. generate-schema ignores them. Create your own .jsonc file without the .example suffix instead.
In cms/{storeId}/components, create cms_component__productshelf.jsonc. Declare every native field of ProductShelf, then append your custom field:
cms/{storeId}/components/cms_component__productshelf.jsonc

_111
{
_111
"$extends": ["#/$defs/base-component"],
_111
"$componentKey": "ProductShelf",
_111
"$componentTitle": "Product Shelf",
_111
"title": "Product Shelf",
_111
"description": "Add custom shelves to your store",
_111
"type": "object",
_111
"required": ["title", "numberOfItems", "after", "sort"],
_111
"properties": {
_111
"title": { "type": "string", "title": "Title" },
_111
"numberOfItems": {
_111
"type": "integer",
_111
"title": "Total number of items",
_111
"default": 5,
_111
"description": "Total number of items. The quantity may be smaller if the query returns fewer products."
_111
},
_111
"itemsPerPage": {
_111
"type": "integer",
_111
"title": "Number of items per page",
_111
"default": 5,
_111
"description": "Number of items to display per page in carousel"
_111
},
_111
"after": {
_111
"type": "string",
_111
"title": "After",
_111
"default": "0",
_111
"description": "Initial pagination item"
_111
},
_111
"sort": {
_111
"title": "Sort",
_111
"description": "Items order",
_111
"default": "score_desc",
_111
"enum": [
_111
"discount_desc", "name_asc", "name_desc", "orders_desc",
_111
"price_asc", "price_desc", "release_desc", "score_desc"
_111
],
_111
"enumNames": [
_111
"Discount: higher to lower", "Name: A-Z", "Name: Z-A",
_111
"Orders: higher to lower", "Price: lower to higher",
_111
"Price: higher to lower", "Release date: newer to older",
_111
"Relevance: higher to lower"
_111
]
_111
},
_111
"term": { "type": "string", "title": "Search term" },
_111
"selectedFacets": {
_111
"title": "Facets",
_111
"type": "array",
_111
"items": {
_111
"title": "Facet",
_111
"type": "object",
_111
"required": ["key", "value"],
_111
"properties": {
_111
"key": {
_111
"title": "Key",
_111
"description": "For collections use: productClusterIds",
_111
"type": "string",
_111
"default": "productClusterIds"
_111
},
_111
"value": {
_111
"title": "Value",
_111
"description": "The ID of the VTEX Collection to pull products from. Verify it exists and has products under Catalog > Collections before using it here.",
_111
"type": "string",
_111
"default": "140"
_111
}
_111
}
_111
}
_111
},
_111
"taxesConfiguration": {
_111
"title": "Taxes Configuration",
_111
"type": "object",
_111
"properties": {
_111
"usePriceWithTaxes": {
_111
"title": "Should use taxes to calculate the price?",
_111
"type": "boolean",
_111
"default": false
_111
},
_111
"taxesLabel": {
_111
"title": "Tax label to be displayed",
_111
"type": "string",
_111
"default": "Tax included"
_111
}
_111
}
_111
},
_111
"productCardConfiguration": {
_111
"title": "Product Card Configuration",
_111
"type": "object",
_111
"properties": {
_111
"showDiscountBadge": {
_111
"title": "Show discount badge?",
_111
"type": "boolean",
_111
"default": true
_111
},
_111
"bordered": {
_111
"title": "Cards should be bordered?",
_111
"type": "boolean",
_111
"default": true
_111
},
_111
"showPixDiscount": {
_111
"title": "Show Pix discount?",
_111
"description": "Displays a \"% off paying with Pix\" message below the product card, using the same discount already applied to the product.",
_111
"type": "boolean",
_111
"default": false
_111
}
_111
}
_111
}
_111
},
_111
"readOnly": false,
_111
"writeOnly": false,
_111
"deprecated": false,
_111
"$abstract": false
_111
}

Step 3 - Register the override

In src/components/index.tsx, directly under src/, map the native section name to your component:
src/components/index.tsx

_10
import ProductShelf from './sections/ProductShelf'
_10
_10
export default {
_10
ProductShelf,
_10
}

src/components/index.tsx must use a default export only. Named exports are not picked up.
The object key connects your code to the CMS definition and must match the $componentKey exactly. When your component name differs from the native section name, map it explicitly:

_10
import CustomProductDetails from './sections/CustomProductDetails'
_10
_10
export default {
_10
ProductDetails: CustomProductDetails,
_10
}

Step 4 - Sync the schema with the CMS

  1. Confirm the component compiles by running yarn dev.
  2. Make sure you're logged in to the correct account (vtex whoami confirms).
  3. From the root of your FastStore project, run:

    _10
    yarn cms-sync

  4. Confirm the override:

    _10
    You are about to override default definitions for the following components:
    _10
    ProductShelf
    _10
    Are you sure? (y/N)

    This command publishes immediately to your live store. Confirm that the store ID matches api.storeId or contentSource.project in discovery.config.js before confirming. Uploading to the wrong store overwrites that store's schema.
  5. Confirm your definition is present in the generated schema.json under cms/{storeId}, in components.ProductShelf.properties.productCardConfiguration.properties, including both the native fields and showPixDiscount.
    Never edit schema.json by hand. It is generated output. If a field is missing, fix the .jsonc file and run yarn cms-sync again.

Step 5 - Verify in the CMS

  1. In the Admin, go to Storefront > Content > All content and select the entry that uses the section, such as Home.
  2. Open the Product Shelf section and confirm that both the native fields and your new Show Pix discount? field appear under Product Card Configuration.
  3. Enable it and click Save. Publish or promote the change if your CMS uses separate draft and live branches.
    {"base64":"  ","img":{"width":1603,"height":773,"type":"gif","mime":"image/gif","wUnits":"px","hUnits":"px","length":1376006,"url":"https://vtexhelp.vtexassets.com/assets/docs/src/show-pix-toogle___579842a11b21c9cca0df58a587a7d2b4.gif"}}
  4. With the development server still running, reload the page. CMS content changes take effect on the next request in local development. You only need to restart the server after code changes.
Uploading a schema registers the definition so it appears in the editor. It doesn't place the section on a page or guarantee that the section has products to show.