React Components for VTEX IO
Guide for creating custom React components in VTEX IO Store Theme projects (builder react: 3.x).
Prerequisites
The manifest.json must contain:
"dependencies": {
"vtex.css-handles": "0.x",
"vtex.render-runtime": "8.x"
}
And in react/typings/:
// vtex.css-handles.ts
declare module 'vtex.css-handles'
File structure
react/
├── MyComponent.js ← entry-point (import/export)
├── components/
│ └── MyComponent/
│ ├── index.js ← main logic
│ ├── schema.js ← VTEX schema (editable props in Site Editor)
│ ├── styles.css ← local CSS (when using CSS Handles)
│ └── components/ ← internal subcomponents (optional)
store/
├── interfaces.json ← maps block → React component
└── blocks/react-components/
└── my-component/
└── my-component.jsonc ← block declaration
Figma integration (optional)
When the user provides a Figma URL, extract the layout before coding the component.
Parse the URL
From https://figma.com/design/:fileKey/:fileName?node-id=1-2:
fileKey=:fileKeynodeId=1:2(replace-with:)
For branch URLs https://figma.com/design/:fileKey/branch/:branchKey/:fileName, use branchKey as fileKey.
Step A — Get design context
server: user-Figma
toolName: get_design_context
arguments: {
"fileKey": "<fileKey>",
"nodeId": "<nodeId>",
"clientLanguages": "javascript,css",
"clientFrameworks": "react"
}
Returns: reference code (React + Tailwind), screenshot, and metadata.
IMPORTANT: The returned code is a REFERENCE, not final code. Adapt it to VTEX IO patterns:
- Replace Tailwind classes → CSS Handles (
useCssHandles) - Replace standard HTML → VTEX block structure
- Replace hardcoded values →
schema.jseditable props - Replace image URLs → VTEX assets or props
Step B — Get screenshot (if needed separately)
server: user-Figma
toolName: get_screenshot
arguments: { "fileKey": "<fileKey>", "nodeId": "<nodeId>" }
Step C — Explore structure (large designs)
For complex designs, get the node tree first:
server: user-Figma
toolName: get_metadata
arguments: {
"fileKey": "<fileKey>",
"nodeId": "<nodeId>",
"clientLanguages": "javascript,css",
"clientFrameworks": "react"
}
Then call get_design_context on specific child nodes.
Figma → VTEX IO mapping
| Figma concept | VTEX IO equivalent |
|---|---|
| Frame / Auto-layout | flex-layout.row / flex-layout.col with blockClass |
| Text layer | Prop in schema.js (editable in Site Editor) |
| Color / font | CSS Handle + theme style in styles/css/ |
| Image | Prop imageUrl or VTEX image#block |
| Button | Prop + CSS Handle or VTEX link.product |
| Spacing / padding | CSS on the handle (e.g. padding, gap) |
| Component variant | withModifiers on the CSS Handle |
| Responsive breakpoint | Media queries in styles.css or VTEX responsive blocks |
Workflow when Figma URL is provided
- Call
get_design_contextwith the URL - Analyze the screenshot and reference code
- Map Figma layers → CSS Handles (name them semantically)
- Map text content →
schema.jsprops (Portuguese titles) - Create the component following the standard creation workflow below
- Style the handles to match the Figma design
Creation workflow
1. Register in interfaces.json
{
"n1-my-component": {
"component": "MyComponent"
}
}
For blocks with children:
{
"n1-my-component": {
"component": "MyComponent",
"composition": "children",
"allowed": "*"
}
}
2. Create entry-point (react/MyComponent.js)
import MyComponent from './components/MyComponent'
export default MyComponent
3. Create component with CSS Handles
import React from 'react'
import { useCssHandles } from 'vtex.css-handles'
const CSS_HANDLES = [
'container',
'title',
'description',
'actionButton',
]
function MyComponent({ heading, subtitle }) {
const { handles, withModifiers } = useCssHandles(CSS_HANDLES)
return (
<div className={handles.container}>
<h2 className={handles.title}>{heading}</h2>
<p className={handles.description}>{subtitle}</p>
<button className={withModifiers('actionButton', ['primary'])}>
Action
</button>
</div>
)
}
export default MyComponent
4. VTEX Schema (schema.js)
Title and description always in Portuguese (store editor language). Uses JSON Schema format — see reference.md for all schema types.
export const schema = {
title: 'Meu Componente',
description: 'Descrição do componente em português',
type: 'object',
properties: {
heading: { title: 'Título', type: 'string', default: 'Olá mundo' },
isVisible: { title: 'Exibir', type: 'boolean', default: true },
image: {
title: 'Imagem', type: 'string', default: '',
widget: { "ui:widget": "image-uploader" },
},
color: {
title: 'Cor', type: 'string', default: 'red',
enum: ['red', 'blue', 'black'],
enumNames: ['Vermelho', 'Azul', 'Preto'],
},
links: {
title: 'Links', type: 'array', maxItems: 6, default: [],
items: {
type: 'object', title: 'Link',
properties: {
__editorItemTitle: { title: 'Nome do item', type: 'string', default: 'Link' },
text: { title: 'Texto', type: 'string', default: '' },
url: { title: 'URL', type: 'string', default: '' },
},
},
},
},
}
Available types: string, boolean, number, object, array.
Widgets: image-uploader, datetime, textarea, color.
Dropdown: use enum + enumNames for labeled selects.
Arrays: use items with sub-properties; add __editorItemTitle for editable names.
Conditional fields: use dependencies + oneOf to show/hide props based on a toggle or selection.
Reusable schemas: use store/contentSchemas.json with definitions and $ref for complex apps.
Bind to the component:
import { schema } from './schema'
MyComponent.schema = schema
MyComponent.defaultProps = {
heading: schema.properties.heading.default,
isVisible: schema.properties.isVisible.default,
}
Array props (lists of items)
When a component accepts a repeatable list of items (links, banners, slides, etc.), use type: 'array' with items defining the shape of each entry. NEVER use flat "slot" props like item1Text, item2Text — always use a proper JSON Schema array.
The VTEX Site Editor renders arrays as an "add item" UI, where each item expands into its own form.
const linkItemSchema = {
type: 'object',
properties: {
text: {
title: 'Texto do link',
type: 'string',
default: '',
},
url: {
title: 'URL de destino',
type: 'string',
default: '',
},
newTab: {
title: 'Abrir em nova aba',
type: 'boolean',
default: false,
},
},
}
export const schema = {
title: 'Meu Componente com Lista',
type: 'object',
properties: {
links: {
title: 'Links',
type: 'array',
maxItems: 6,
items: linkItemSchema,
default: [],
},
},
}
Key rules for arrays:
itemsdefines the schema each array entry must follow (like JSON Schema spec)maxItemslimits how many entries the editor allows (optional but recommended)minItemssets a minimum (optional)defaultshould be[](empty array)- In the component, always guard with
Array.isArray(prop)before mapping - NEVER create flat numbered props (
item1,item2, etc.) — use arrays instead
5. Declare JSONC block
// store/blocks/react-components/my-component/my-component.jsonc
{
"n1-my-component#default": {
"title": "My Component",
"props": {
"heading": "Welcome"
}
}
}
CSS Handles — Rules
Naming conventions
- Names in camelCase:
containerWrapper,priceLabel,actionButton - Semantic: describe what the element is, not how it looks
- Prefix with context when needed:
modalOverlay,modalTitle,modalClose
useCssHandles — basic usage
import { useCssHandles } from 'vtex.css-handles'
const CSS_HANDLES = ['container', 'title'] as const
function Component({ classes }) {
const { handles, withModifiers } = useCssHandles(CSS_HANDLES, { classes })
return <div className={handles.container}>...</div>
}
The generated class follows the format: vtex-{appVendor}-{appName}-{major}-x-{handleName}
withModifiers — states and variants
Useful for visual states (active/inactive, variants, colors):
<div className={withModifiers('item', [isActive && 'active', color])}>
Generates classes like: item item--active item--blue
migrationFrom — migration between apps
When a component migrates from one app to another, preserve legacy handles:
const handles = useCssHandles(CSS_HANDLES, {
migrationFrom: 'vtex.search-result@3.x',
classes,
})
createCssHandlesContext — components with subcomponents
When multiple components of the same block need to share handles, avoid prop drilling:
// cssHandlesContext.js
import { createCssHandlesContext } from 'vtex.css-handles'
import { CSS_HANDLES } from './PublicComponent'
export const { CssHandlesProvider, useContextCssHandles } =
createCssHandlesContext(CSS_HANDLES)
Root component:
export const CSS_HANDLES = ['root', ...SubComponent.cssHandles]
function PublicComponent({ classes }) {
const { handles } = useCssHandles(CSS_HANDLES, { classes })
return (
<CssHandlesProvider handles={handles}>
<SubComponent />
</CssHandlesProvider>
)
}
Subcomponent:
import { useContextCssHandles } from './cssHandlesContext'
const CSS_HANDLES = ['nested']
function SubComponent() {
const { handles } = useContextCssHandles()
return <div className={handles.nested}>...</div>
}
SubComponent.cssHandles = CSS_HANDLES
Alternative approach: local CSS (import styles)
For components that do not need to expose handles for external customization:
import styles from './styles.css'
function Component() {
return <div className={styles.container}>...</div>
}
The styles.css file declares empty classes that the bundler transforms into scoped classes:
.container {}
.title {}
When to use each approach:
| Scenario | Approach |
|---|---|
| Component that other apps will customize | useCssHandles |
| Isolated component, self-contained styling | import styles from './styles.css' |
| Migration between apps | useCssHandles with migrationFrom |
| Block with deeply nested subcomponents | createCssHandlesContext |
Theme styling
Component styles live in styles/scss/ or styles/css/:
// styles/scss/react-components/my-component/vtex.flex-layout.scss
.flexRow--my-component-container :global(.lojamm-store-theme-0-x-title) {
font-size: 20px;
font-weight: 700;
}
The selector follows: .{vtexParentBlock}--{blockClass} :global(.{fullHandleName})
SSR and canUseDOM
For code that accesses window, document or localStorage:
import { canUseDOM } from 'vtex.render-runtime'
useEffect(() => {
if (!canUseDOM) return
// safe client-side code
}, [])
Checklist
- Entry-point at
react/ComponentName.js - Component at
react/components/ComponentName/index.js - Registered in
store/interfaces.json - Block declared in
store/blocks/react-components/ - CSS Handles declared as
constwith camelCase names - Schema with title/description in Portuguese
- Repeatable items use
type: 'array'withitems— never flat slot props -
canUseDOMbefore accessing browser APIs - defaultProps derived from schema
Additional resources
- For the full CSS Handles API, see reference.md
- For real project examples, see examples.md