references/analytics-events-and-gtm.md
---
name: faststore-analytics
description: How to send and receive analytics events using @faststore/sdk, and how to configure Google Tag Manager in a FastStore storefront. Use when adding analytics tracking, firing custom events on user interactions, listening for events in a handler component, or setting up GTM.
metadata:
author: vtex
version: "1.0"
---
# FastStore Analytics
FastStore provides an analytics module via `@faststore/sdk` for sending and receiving events.
## Imports
```typescript
import { sendAnalyticsEvent, useAnalyticsEvent } from "@faststore/sdk";
```
- `sendAnalyticsEvent` — Dispatches a custom event to all registered analytics handlers
- `useAnalyticsEvent` — Hook that listens for analytics events (used in handler components)
## Sending Events
Call `sendAnalyticsEvent` on user interactions:
```tsx
import { sendAnalyticsEvent } from "@faststore/sdk";
interface ArbitraryEvent {
name: string;
isEcommerceEvent?: boolean;
params: Record<string, any>;
}
const onSubmit = async (event: FormEvent) => {
event.preventDefault();
sendAnalyticsEvent<ArbitraryEvent>({
name: "Submit Newsletter",
params: {
form_location: "custom_newsletter_section",
user_agent: navigator.userAgent,
},
});
// After successful action
sendAnalyticsEvent<ArbitraryEvent>({
name: "Submit newsletter success",
params: { campaign: "newsletter_signup", source: "organic" },
});
};
```
## Receiving Events — Analytics Handler Component
Place an `AnalyticsHandler` component inside your section to capture and forward events to your analytics provider:
```tsx
import { useAnalyticsEvent } from "@faststore/sdk";
interface ArbitraryEvent {
name: string;
isEcommerceEvent?: boolean;
params: Record<string, any>;
}
export const AnalyticsHandler = () => {
useAnalyticsEvent((event: ArbitraryEvent) => {
// Forward to your analytics provider (GTM, GA4, Segment, etc.)
// In development, console.log is fine for debugging
console.log("Received event", event);
// Example: push to GTM dataLayer
// window.dataLayer?.push({ event: event.name, ...event.params });
});
return null; // Renders nothing — only listens
};
```
## Full Example — Custom Newsletter with Analytics
```tsx
// src/components/sections/CustomNewsletter/CustomNewsletter.tsx
import { FormEvent } from "react";
import { sendAnalyticsEvent, useAnalyticsEvent } from "@faststore/sdk";
interface NewsletterEvent {
name: string;
params: { form_location: string; [key: string]: any };
}
const AnalyticsHandler = () => {
useAnalyticsEvent((event: NewsletterEvent) => {
console.log("Analytics event:", event);
});
return null;
};
function CustomNewsletter() {
const onSubmit = async (event: FormEvent) => {
event.preventDefault();
sendAnalyticsEvent<NewsletterEvent>({
name: "newsletter_subscribe",
params: { form_location: "custom_newsletter_section" },
});
};
return (
<section>
<AnalyticsHandler />
<form onSubmit={onSubmit}>
{/* form fields */}
</form>
</section>
);
}
export default CustomNewsletter;
```
## Google Tag Manager
GTM is configured in `discovery.config.js`:
```js
analytics: {
gtmContainerId: "GTM-XXXXXXX", // Replace with your actual GTM container ID
},
```
FastStore Core automatically injects the GTM script using this container ID. No manual script injection is needed for GTM itself.
references/cms-schema-and-section-registration.md
---
name: faststore-cms
description: How VTEX Headless CMS integrates with FastStore, including how to define section schema.json and sync them. Use when registering new CMS sections, defining editable CMS fields (text, boolean, dropdown, nested objects), syncing schema changes, or understanding how CMS props flow to React components.
metadata:
author: vtex
version: "1.0"
---
# FastStore CMS Integration
## Overview
**Authoritative inputs** for Headless CMS sections are:
- `cms/faststore/components/*.jsonc` — section schema (including `"$componentKey"`)
- `cms/faststore/pages/*.jsonc` — optional page templates (when your project uses them)
- `src/components/index.tsx` — **default export** object whose **keys** must match `"$componentKey"` for each custom section
The file **`cms/faststore/schema.json` is generated output** from `vtex content generate-schema`. It aggregates those sources for upload. **Never edit `schema.json` by hand** — fix the JSONC and/or `index.tsx`, then regenerate.
**All global native sections are already registered** in the platform; your repo extends the CMS with custom definitions.
To edit **content** in existing sections, use the Store Admin at:
`https://{store-id}.myvtex.com/admin` → `Storefront` → `Content: All content`
## Critical Rules
1. There is no need of creating `cms/faststore/pages/*.jsonc` files for new sections. This should be edited only when a new landing page is needed.
2. After every change to `cms/faststore/components/*.jsonc` or `cms/faststore/pages/*.jsonc`, you **must** run **`vtex content generate-schema`** and **`vtex content upload-schema`** in the same working session (see [End-to-end agent workflow](#end-to-end-agent-workflow)). **Do not** use `yarn cms-sync`, `faststore cms-sync`, or other legacy `cms-sync` flows to publish Headless CMS schema — use **`vtex content`** only.
3. Every file inside the folder `cms/faststore/components/` should follow this name pattern and extension: `cms_component__<name>.jsonc`
4. Follow the conventions of the native components in `node_modules/@faststore/core/cms/faststore/components/` — see [Native Component Pattern Reference](#native-component-pattern-reference) for the full style guide
5. **Every custom section MUST use the `section` class as its first CSS class** on the root `<section>` element. This class provides the standard FastStore section spacing, padding, and responsive behavior. Always place it before any component-specific classes.
6. **Every custom section MUST have an inner `<div className="layout__content">` wrapper** immediately inside the `<section>` element. This wrapper constrains the content to the store's max-width grid and centers it. Without it, content will stretch edge-to-edge and break the store layout.
**Correct section structure:**
```tsx
<section className={`section ${styles.mySection}`}>
<div className="layout__content">{/* Section content goes here */}</div>
</section>
```
**Wrong — missing `section` class and `layout__content` wrapper:**
```tsx
<section className={styles.mySection}>
{/* Content renders without standard spacing and full-bleed */}
</section>
```
## Native Component Pattern Reference
When creating a new `cms/faststore/components/cms_component__<name>.jsonc`, **follow the conventions used by the native components** in `node_modules/@faststore/core/cms/faststore/components/`. The patterns below are extracted from those files and must be treated as the canonical style guide.
### Structural conventions
1. **Top-level keys appear in this order** — always:
```jsonc
{
"$extends": ["#/$defs/base-component"],
"$componentKey": "MySection",
"$componentTitle": "My Section",
"title": "My Section",
"description": "Short CMS editor description",
"type": "object",
"required": [...],
"properties": { ... }
}
```
- `$extends` is always `["#/$defs/base-component"]`.
- `$componentKey` is **PascalCase with no spaces** (e.g. `"ProductShelf"`, `"BannerText"`).
- `$componentTitle` and `"title"` are **human-readable** (may contain spaces): `"Product Shelf"`, `"Banner Text"`.
- `"description"` is a short sentence shown in the CMS palette (e.g. `"Add a quick promotion with an image/action pair"`).
- `"required"` lists only the fields that **must** be filled by the editor; optional fields are simply omitted from this array.
2. **File name** matches the lowercased component name: `cms_component__productshelf.jsonc` for `$componentKey: "ProductShelf"`.
### Property conventions
| Pattern | Convention | Example from core |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| Simple text field | `{ "type": "string", "title": "Title" }` | Hero → `title` |
| Text with default | Add `"default"` at the same level | Newsletter → `emailInputLabel` |
| Rich text (WYSIWYG) | `"widget": { "ui:widget": "draftjs-rich-text" }` | Newsletter → `privacyPolicy` |
| Image upload | `"widget": { "ui:widget": "media-gallery", "restrictMediaTypes": { "video": true, "image": ["png","jpg","jpeg","gif","svg","webp"] } }` | Hero → `image.src` |
| Boolean toggle | `{ "type": "boolean", "title": "...", "default": false }` | Alert → `dismissible` |
| Dropdown (enum) | `"enum"` + `"enumNames"` arrays of equal length; `enum` holds the value, `enumNames` the label | Hero → `colorVariant` (`["main","light","accent"]` / `["Main","Light","Accent"]`) |
| Integer with default | `{ "type": "integer", "title": "...", "default": 5 }` | ProductShelf → `numberOfItems` |
| Nested object group | `{ "type": "object", "title": "...", "properties": { ... } }` — nest `required` inside the object when needed | BannerText → `link` (with inner `required: ["text","url"]`) |
| Repeatable list | `{ "type": "array", "items": { "type": "object", ... } }` — use `minItems`/`maxItems` to constrain | Incentives → `incentives` (array of incentive objects) |
| Sub-object config group | Group related toggles/fields under a descriptive object | ProductShelf → `taxesConfiguration`, `productCardConfiguration` |
### Key rules derived from native components
- **Every property must have `"title"`** — it is the CMS editor label.
- **Use `"default"` generously** — provide sensible defaults so editors start with a working section.
- **`"description"` on a property** is optional but recommended when the field is not self-explanatory (e.g. ProductShelf → `after`: `"Initial pagination item"`).
- **Enums always use both `"enum"` and `"enumNames"`** — even when the display name matches the value. The arrays must have the same length and order.
- **Nested objects with required inner fields** place the `"required"` array inside the object definition, not at the root level.
- **No trailing commas in JSON** — although `.jsonc` tolerates them, the native components do **not** use trailing commas. Follow the same style.
- **`"type"` is always explicit** on every property, including nested objects and array items.
### Complete annotated example (following native style)
```jsonc
{
"$extends": ["#/$defs/base-component"],
"$componentKey": "PromoBanner",
"$componentTitle": "Promo Banner",
"title": "Promo Banner",
"description": "Display a promotional banner with image and call to action",
"type": "object",
"required": ["title", "image"],
"properties": {
"title": {
"title": "Title",
"type": "string",
},
"subtitle": {
"title": "Subtitle",
"type": "string",
},
"image": {
"title": "Image",
"type": "object",
"properties": {
"src": {
"title": "Image",
"type": "string",
"widget": {
"ui:widget": "media-gallery",
"restrictMediaTypes": {
"video": true,
"image": ["png", "jpg", "jpeg", "gif", "svg", "webp"],
},
},
},
"alt": {
"title": "Alternative Label",
"type": "string",
},
},
},
"link": {
"title": "Call to Action",
"type": "object",
"required": ["text", "url"],
"properties": {
"text": {
"title": "Text",
"type": "string",
},
"url": {
"title": "URL",
"type": "string",
},
"linkTargetBlank": {
"title": "Open link in new window?",
"type": "boolean",
"default": false,
},
},
},
"colorVariant": {
"title": "Color variant",
"type": "string",
"enumNames": ["Main", "Light", "Accent"],
"enum": ["main", "light", "accent"],
},
"showBadge": {
"title": "Show discount badge?",
"type": "boolean",
"default": true,
},
"items": {
"title": "Highlight Items",
"type": "array",
"minItems": 1,
"maxItems": 4,
"items": {
"title": "Item",
"type": "object",
"required": ["label"],
"properties": {
"label": {
"title": "Label",
"type": "string",
},
"icon": {
"title": "Icon",
"type": "string",
"enumNames": ["Truck", "Gift", "Shield Check"],
"enum": ["Truck", "Gift", "ShieldCheck"],
},
},
},
},
},
}
```
## Mandatory Workflow for New Custom Sections
Follow this EXACT sequence. Do NOT skip steps.
### Phase 1: Planning (BEFORE writing code)
- [ ] Check if similar component exists: `ls src/components/`
- [ ] Verify CMS schema names: `ls cms/faststore/components/`
- [ ] Choose unique component name (PascalCase)
### Phase 2: Component Creation
- [ ] Create folder: `mkdir -p src/components/sections/<Name>` (or `src/components/<Name>/` for non-section sub-components)
- [ ] Create React component at `src/components/sections/<Name>/<Name>.tsx` with TypeScript interfaces
- The root element MUST be `<section className={\`section ${styles.mySection}\`}>`—`section` class always comes first
- Immediately inside the `<section>`, add `<div className="layout__content">` to wrap all content
- [ ] Create styles at `src/components/sections/<Name>/<name>.module.scss`
- Wrap all styles in a single class
- Import as CSS module in the component
- If the section uses `@faststore/ui` components, **import their stylesheets manually** in the `.module.scss`
- [ ] **RUN LINTER**: `ReadLints` on new files
- [ ] **FIX ALL ERRORS** before proceeding
### Phase 3: CMS Schema & Registration
- [ ] Create JSONC schema at `cms/faststore/components/cms_component__<Name>.jsonc` following the [Native Component Pattern Reference](#native-component-pattern-reference)
- [ ] Verify `$componentKey` matches exactly
- [ ] Register in `src/components/index.tsx` with an object key **identical** to `$componentKey`
- [ ] **RUN LINTER** on `index.tsx`
### Phase 4: Schema Management (SAME SESSION)
- [ ] Generate: `vtex content generate-schema -o cms/faststore/schema.json `
- [ ] Verify: `grep -A 5 '"<ComponentName>"' cms/faststore/schema.json` (search for the `$componentKey` string)
- If it does not appear, fix JSONC or registration — **do not** edit `schema.json` manually
- [ ] **Account check** (before upload): read `api.storeId` from `discovery.config.js` and run `vtex whoami` — confirm both match. If they differ, ask the user to run `vtex login <correct-account>` and **stop**.
- [ ] **Ask the user**: _"The schema will be uploaded to account **`<store-id>`**. Do you want to proceed?"_ — wait for confirmation before continuing.
- [ ] Upload: `vtex content upload-schema cms/faststore/schema.json` (**required** — without upload, the CMS editor will not see new or updated section definitions)
- [ ] Confirm upload success message
### Phase 5: Validation & Deployment
- [ ] No linter errors remain
- [ ] Schema uploaded successfully
- [ ] Component key appears in `schema.json`
- [ ] Add the section to the desired page via **Admin → Storefront → Content** (unless your project relies on `pages/*.jsonc` and your team's publish process covers composition)
- [ ] Document usage (optional but recommended)
**🛑 STOP at first error. Fix before proceeding.**
## Section Scopes
Sections can be scoped to specific page types using `"requiredScopes"`:
```json
{
"$extends": ["#/$defs/base-component"],
"$componentKey": "QuickFilter",
"$componentTitle": "QuickFilter",
"requiredScopes": ["plp", "search"],
"type": "object",
"description": "Quick Filter section for search pages",
"required": [],
"properties": {}
}
```
## Section registration ≠ Section rendering
**`upload-schema` registers** section **definitions** in Headless CMS (they show up in the editor palette). That is **not** the same as placing the section on the home page or another route.
To **render** a new section on a page (typical case):
1. Go to **Admin → Storefront → Content** (e.g. "All content")
2. Select the page type (e.g., Home, Product List Page)
3. Add the section to that page's layout
4. Save and publish
If your project uses **`cms/faststore/pages/*.jsonc`** to version page composition, follow that policy — otherwise assume **Content** in Admin is where the section gets onto the live page.
In dev mode, page content still comes from the CMS API. If the section is registered in the editor but missing on the storefront, it often was never **added** to that page's content (or not published).
## End-to-end agent workflow
Assume **VTEX CLI is installed globally** — invoke `vtex` directly (not `npx vtex`).
From the **project root**:
1. **Generate** (canonical command — matches the storefront skill):
```bash
vtex content generate-schema -o cms/faststore/schema.json
```
2. **Validate** — for new or renamed sections, grep or read `cms/faststore/schema.json` and confirm the `"$componentKey"` is present.
3. **Upload** (mandatory for the CMS to pick up schema changes):
```bash
vtex content upload-schema cms/faststore/schema.json
```
The store ID you enter at prompts should match `api.storeId` in `discovery.config.js`.
### Pre-upload account verification (MANDATORY)
**Before every `upload-schema` execution**, the agent **must** verify that the currently logged-in VTEX account matches the project's target store. Uploading to the wrong account overwrites CMS schema in the wrong store — **this is not reversible without manual intervention**.
**Steps:**
1. **Read the expected store ID** from the project config:
```bash
node -e "console.log(require('./discovery.config.js').api.storeId)"
```
2. **Read the currently logged-in account** from the VTEX CLI:
```bash
vtex whoami
```
The output includes the account name (e.g. `Logged into account: mystore`). Extract the account name.
3. **Compare** the two values. If they **do not match**, stop immediately and tell the user:
> ⚠️ The VTEX CLI is logged into account **`<logged-account>`**, but `discovery.config.js` has `api.storeId` set to **`<expected-store-id>`**. Please run `vtex login <expected-store-id>` or switch to the correct account before uploading.
**Do not** proceed with upload-schema until the accounts match.
4. **Even when accounts match**, the agent **must ask the user for explicit confirmation** before uploading:
> The schema will be uploaded to account **`<store-id>`**. Do you want to proceed? (yes/no)
Wait for the user's response. **Only proceed if the user confirms.**
### Non-interactive upload (automatic - USE ONLY AFTER ACCOUNT VERIFICATION)
When uploading schema in an automated workflow, ALWAYS use `expect` to handle prompts automatically. **This block must only run after the pre-upload account verification above has passed and the user has confirmed.**
```bash
# Export store ID from discovery.config.js
export STORE_ID=$(node -e "console.log(require('./discovery.config.js').api.storeId)")
# Run upload with expect to auto-answer prompts
# IMPORTANT: use single quotes so Tcl does not misinterpret $ tokens
# (e.g. $id) that appear in CLI output. $env(STORE_ID) is Tcl syntax
# evaluated by the Tcl interpreter, not by bash.
expect -c '
spawn vtex content upload-schema cms/faststore/schema.json
expect "store ID"
send "faststore\r"
expect -re "uploaded|confirm"
send "y\r"
expect -re "Are you sure|confirm"
send "y\r"
expect eof
' 2>&1
```
**Never use double quotes** around the `expect -c` argument — CLI output often contains `$id` and other `$`-prefixed tokens that Tcl interprets as variable references inside double-quoted strings, causing `can't read "id": no such variable` errors. Single quotes pass the script literally to Tcl, where `$env(STORE_ID)` is evaluated correctly by the Tcl interpreter.
**Agents must** report the **exact** prompt or error if login, workspace, store ID, or confirmation blocks upload, and tell the human the next step (`vtex login`, correct account, etc.).
Without **`upload-schema`**, new or updated sections **will not** appear in the Headless CMS editor.
## Important: CMS Sections Only Receive CMS-Defined Props
CMS sections receive props from the schema `properties` defined in their `.jsonc` file —
these are the fields the editor fills in the CMS admin.
**Do NOT** expect sections to receive props passed programmatically from parent
page components. If a section needs data beyond what the CMS editor provides
(e.g., product data, search results), it must read from:
- Page context hooks (`usePDP()`, `usePLP()`, `usePage()`, etc.)
- Custom GraphQL queries via `useQuery` / `useLazyQuery`
references/extending-graphql-with-custom-resolvers.md
---
name: faststore-api-extension
description: Step-by-step guide to extending the FastStore BFF GraphQL API with custom VTEX type fields or entirely new third-party queries and mutations, and how to consume them in React components. Use when adding new fields to StoreProduct or other built-in types, creating new queries to external APIs, adding form submission mutations, or using useQuery/useLazyQuery hooks to fetch custom data.
metadata:
author: vtex
version: "1.0"
---
# FastStore GraphQL API Extensions
FastStore exposes a GraphQL BFF layer that proxies between your storefront and the VTEX platform APIs. There are two extension mechanisms:
1. **VTEX extensions** (`src/graphql/vtex/`) — Extend existing FastStore API types (e.g., `StoreProduct`) with additional fields resolved from VTEX root data already available in the resolver context.
2. **Third-party extensions** (`src/graphql/thirdParty/`) — Define entirely new types, queries, and mutations that call external APIs.
## Directory Structure
```
src/graphql/
├── vtex/ # Extensions to existing FastStore/VTEX types
│ ├── typeDefs/
│ │ └── product.graphql # Schema extensions (extend type StoreProduct, etc.)
│ └── resolvers/
│ ├── product.ts # Resolver for extended fields
│ └── index.ts # Aggregates all VTEX resolvers
└── thirdParty/ # New types, queries, and mutations from external APIs
├── typeDefs/
│ ├── query.graphql
│ └── contactForm.graphql
└── resolvers/
├── queries.ts
├── contactForm.ts
└── index.ts
```
---
## Extending Existing VTEX Types
Use when you need to add fields to types FastStore already provides (e.g., adding installment data to `StoreProduct`).
### Step 1 — Define the Schema Extension
```graphql
# src/graphql/vtex/typeDefs/product.graphql
type Installments {
installmentPaymentSystemName: String!
installmentValue: Float!
installmentInterest: Float!
installmentNumber: Float!
}
extend type StoreProduct {
availableInstallments: [Installments!]!
}
```
Use `extend type <ExistingType>` to add fields to FastStore's built-in types.
### Step 2 — Create the Resolver
```typescript
// src/graphql/vtex/resolvers/product.ts
import type { StoreProductRoot } from "@faststore/core/api";
const productResolver = {
StoreProduct: {
availableInstallments: (root: StoreProductRoot) => {
const installments = root.sellers?.[0]?.commertialOffer?.Installments;
if (!installments?.length) return [];
return installments.map((installment) => ({
installmentPaymentSystemName: installment.PaymentSystemName,
installmentValue: installment.Value,
installmentInterest: installment.InterestRate,
installmentNumber: installment.NumberOfInstallments,
}));
},
},
};
export default productResolver;
```
Import root types from `@faststore/core/api` (e.g., `StoreProductRoot`, `StoreCollectionRoot`).
### Step 3 — Register the Resolver
```typescript
// src/graphql/vtex/resolvers/index.ts
import { default as StoreProductResolver } from "./product";
const resolvers = { ...StoreProductResolver };
export default resolvers;
```
### Step 4 — Add Fragments to Include New Fields in Page Queries
Fragment filenames must match the query they extend:
```typescript
// src/fragments/ServerProduct.ts
import { gql } from "@faststore/core/api";
export const fragment = gql(`
fragment ServerProduct on Query {
product(locator: $locator) {
availableInstallments {
installmentPaymentSystemName
installmentValue
installmentInterest
installmentNumber
}
}
}
`);
```
```typescript
// src/fragments/ClientProduct.ts — must mirror ServerProduct
import { gql } from "@faststore/core/api";
export const fragment = gql(`
fragment ClientProduct on Query {
product(locator: $locator) {
availableInstallments {
installmentPaymentSystemName
installmentValue
installmentInterest
installmentNumber
}
}
}
`);
```
#### Fragment Naming Convention
| Filename | Extends query for |
|----------|-------------------|
| `ServerProduct.ts` | Server-side PDP query |
| `ClientProduct.ts` | Client-side PDP query |
| `ClientProductGallery.ts` | Client-side PLP query |
| `ClientManyProducts.ts` | — |
| `ClientSearchSuggestions.ts` | Search autocomplete query |
| `ClientShippingSimulation.ts` | Shipping simulation query |
| `ClientTopSearchSuggestions.ts` | Top search query |
| `ClientCollectionPage.ts` | Client-side PLP/collection query |
| `ServerCollectionPage.ts` | Server-side PLP/collection query |
### Consuming VTEX Extensions in React
VTEX type extensions are automatically available through FastStore's built-in page hooks — no extra client-side query needed:
```tsx
import { usePDP } from "@faststore/core";
function ProductInstallments() {
const context = usePDP();
const installment = context?.data?.product?.availableInstallments[0];
if (!installment || installment.installmentInterest !== 0) return null;
return (
<span>
{installment.installmentNumber} interest-free installments
of ${installment.installmentValue}
</span>
);
}
```
---
## Third-Party Extensions — New Queries
Use when you need to fetch data from external (non-VTEX) APIs.
### Step 1 — Define the Schema
```graphql
# src/graphql/thirdParty/typeDefs/query.graphql
type CEP {
cep: String!
logradouro: String!
bairro: String!
localidade: String!
uf: String!
estado: String!
regiao: String!
ddd: String!
complemento: String
unidade: String
ibge: String
gia: String
siafi: String
}
extend type Query {
searchCEP(CEP: String!): CEP!
}
```
Use `extend type Query` to add new query fields.
### Step 2 — Create the Resolver
```typescript
// src/graphql/thirdParty/resolvers/queries.ts
import { Query } from "@faststore/core/api";
export default {
Query: {
searchCEP: async (_: unknown, { CEP }: { CEP: string }): Promise<Query["searchCEP"]> => {
const resp = await fetch(`http://viacep.com.br/ws/${CEP}/json`);
return resp.json();
},
},
};
```
### Step 3 — Register
```typescript
// src/graphql/thirdParty/resolvers/index.ts
import queriesResolver from "./queries";
const resolvers = { ...queriesResolver };
export default resolvers;
```
---
## Third-Party Extensions — New Mutations
Use when you need to send data to external services (form submissions, writes, etc.).
### Step 1 — Define the Schema
```graphql
# src/graphql/thirdParty/typeDefs/contactForm.graphql
type ContactFormResponse {
message: String!
}
input ContactFormInput {
name: String!
email: String!
subject: String!
message: String!
}
# Use `type Mutation` for the first mutation definition,
# `extend type Mutation` if another file already defines it.
type Mutation {
submitContactForm(input: ContactFormInput!): ContactFormResponse
}
```
### Step 2 — Create the Resolver
```typescript
// src/graphql/thirdParty/resolvers/contactForm.ts
type SubmitContactFormData = {
input: { name: string; email: string; subject?: string; message: string };
};
const contactFormResolver = {
Mutation: {
submitContactForm: async (_: never, data: SubmitContactFormData) => {
const { input } = data;
try {
const response = await fetch("https://your-api-endpoint.com/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
});
if (!response.ok) throw new Error("Error while sending the message");
return { message: "Your message was sent successfully!" };
} catch (error) {
return { message: error };
}
},
},
};
export default contactFormResolver;
```
---
## Consuming Custom Queries and Mutations in React
Third-party queries and mutations require explicit hooks from `@faststore/core/experimental`.
### Imports
```typescript
import { gql } from "@faststore/core/api";
import { useQuery_unstable as useQuery } from "@faststore/core/experimental";
import { useLazyQuery_unstable as useLazyQuery } from "@faststore/core/experimental";
```
### `useQuery` — Auto-Executing Queries
Fires on mount, re-executes when variables change. SWR-powered (caching, revalidation, deduplication).
```tsx
const SEARCH_CEP_QUERY = gql`
query getCEPQuery($cep: String!) {
searchCEP(CEP: $cep) {
logradouro
bairro
localidade
uf
}
}
`;
function AddressLookup({ cep }: { cep: string }) {
const { data, error } = useQuery(SEARCH_CEP_QUERY, { cep });
if (error) return <p>Failed to load.</p>;
if (!data) return <p>Loading...</p>;
return <address>{data.searchCEP.logradouro}, {data.searchCEP.localidade}</address>;
}
```
### `useLazyQuery` — Deferred / Imperative Execution
Returns `[execute, response]`. Use for mutations and user-triggered queries.
```tsx
const SUBMIT_CONTACT_FORM = gql`
mutation SubmitContactForm($input: ContactFormInput!) {
submitContactForm(input: $input) { message }
}
`;
function ContactForm() {
const [execute, { data, error }] = useLazyQuery(SUBMIT_CONTACT_FORM, {
input: { name: "", email: "", subject: "", message: "" },
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await execute({ input: formData });
};
// ...
}
```
### `gql` Tag Rules
1. **Operation names matter** — queries must end with `Query` (e.g., `getCEPQuery`) for FastStore to use HTTP GET; mutations default to POST
2. **One operation per `gql` tag**
3. **`gql` calls must be at module scope** — not inside component bodies
---
## Quick Reference
| Goal | Schema location | Resolver location | Consumption |
|------|----------------|-------------------|-------------|
| Add fields to existing FastStore type | `src/graphql/vtex/typeDefs/*.graphql` | `src/graphql/vtex/resolvers/` | `usePDP()`, `usePLP()`, etc. |
| New query to external API | `src/graphql/thirdParty/typeDefs/*.graphql` | `src/graphql/thirdParty/resolvers/` | `useQuery` |
| New mutation to external API | `src/graphql/thirdParty/typeDefs/*.graphql` | `src/graphql/thirdParty/resolvers/` | `useLazyQuery` |
## Checklist
1. Create the `.graphql` schema file in the appropriate `typeDefs/` directory
2. Create the TypeScript resolver file in the matching `resolvers/` directory
3. Register the resolver by spreading it into the corresponding `resolvers/index.ts`
4. For third-party queries/mutations: define a `gql`-tagged operation at module scope in your component
5. Use `useQuery` (auto-execute) or `useLazyQuery` (execute on demand) to consume the data
6. Restart the dev server — schema changes require a rebuild to regenerate types
references/faststore-v3-v4-migration.md
# FastStore v3 → v4 Migration
A reusable guide for migrating any VTEX FastStore storefront from v3 to v4.
Written from two real migrations; all store/account names below are
placeholders — substitute the values for the store being migrated.
Conventions used here:
- `<store>` — the storefront repo being migrated.
- `<store>` is assumed to be a yarn-workspaces monorepo with the FastStore app
under `packages/discovery` (a single-package store has the same files at the
repo root — adjust paths accordingly).
- `@vtex/faststore-plugin-buyer-portal` is the B2B plugin; a store without it
simply skips every plugin-related step.
---
## 0. Outcome & the two install modes
The goal: `yarn build` and `yarn dev` both succeed on FastStore v4.
Keep these two setups separate:
- **Deployable install** (what gets committed / what CI and production use) —
`package.json` pins *published* package versions; a plain `yarn install`
succeeds with no symlinks and no sibling checkouts. This is the committed
state.
- **Local multi-repo testing** (optional, never committed) — validating the
store against *unpublished* `faststore` / plugin source via a symlink layer
applied on top of a normal install. See §9. Revert to the deployable state
before committing.
---
## 1. Node 24 is mandatory
FastStore v4's dependency tree (e.g. `eslint-visitor-keys@5`) declares
`engines.node` of `>=20.19 || >=22.13 || >=24`. A plain `yarn install` on an
older Node (e.g. 20.12) fails with `Found incompatible module`.
- Use Node 24 for install, build and dev.
- Set `volta.node` to `"24.0.2"` (or the latest Node 24 patch) in `packages/discovery/package.json`.
- Set `experimental.nodeVersion: 24` in `discovery.config.js`.
---
## 2. Deployable `package.json`
The store declares only the VTEX packages and the framework — **never**
hand-list `@faststore/core`'s transitive dependencies; they arrive through
`@faststore/cli`.
`packages/discovery/package.json` → `dependencies` (keep any store-specific
app dependencies — e.g. `crypto-js`, `draft-js` — alongside these):
```json
{
"@faststore/cli": "<published v4 release>",
"@vtex/faststore-plugin-buyer-portal": "<published v4 release>",
"graphql": "^16.11.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.24.1"
}
```
**Important:** do **not** declare `next` in `dependencies` — Next.js is now
managed internally by `@faststore/cli`, and declaring it separately causes
version conflicts. Also update `typescript` in `devDependencies` to `^5.9.3`.
`graphql` must be declared explicitly because it is a `peerDependency` of
`@faststore/cli` — yarn v1 does not install peer dependencies automatically.
Root `package.json`: `@faststore/cli` (devDependency) +
`@vtex/faststore-plugin-buyer-portal` (dependency).
**Monorepo stores only:** add `@inquirer/type` to the `resolutions` field in
the root `package.json` to avoid version conflicts with the `inquirer` package:
```json
{
"resolutions": {
"@inquirer/type": "^1.5.5"
}
}
```
**Pin npm releases, not pkg.pr.new / pkg.csb.dev tarballs.** The faststore v4
monorepo uses pnpm `catalog:` / `workspace:*` specs. `npm publish` (via
`pnpm publish`) resolves these to concrete versions, so npm releases install
cleanly. **pkg.pr.new tarballs do NOT** — they keep `catalog:` unresolved and
`yarn install` fails with `Couldn't find any versions ... matches "catalog:"`.
`@faststore/cli` transitively brings `@faststore/core`, which brings
`@faststore/ui`, `@faststore/api`, `@faststore/sdk`, `@faststore/diagnostics`,
`@faststore/lighthouse`, `@faststore/components`, plus the v4 third-party tree
(Next 16, GraphQL 16, etc.).
> Remove every v3-only hand-listed transitive dependency from
> `packages/discovery/package.json` — they now arrive via `@faststore/cli`.
---
## 3. `discovery.config.js` must be a plain config
v3 stores often start `discovery.config.js` with Node code:
```js
const path = require("path");
const dotenv = require("dotenv");
dotenv.config({ path: path.resolve(__dirname, ".env") });
```
In v4 this file is pulled into the client/instrumentation bundle, where webpack
cannot resolve Node built-ins — `Module not found: Can't resolve 'path'` from
`dotenv` — and routes 500.
- **Remove** the `require("path")` / `require("dotenv")` / `dotenv.config()`
lines. Next.js loads `.env` / `.env.local` automatically; reading
`process.env.NEXT_PUBLIC_*` directly is fine.
- **Remove any v3 `webpack()` callback.** FastStore v4 core owns the webpack
config; in v3, core never invoked `storeConfig.webpack`, so that callback was
already dead code. (Do not try to re-enable it — see §7.)
- Set `experimental.nodeVersion: 24`.
- If the store consumes linked/transpiled local packages, add
`experimental.transpilePackages: [ ... ]`.
---
## 4. SCSS migration: `@import` → `@use` / `@forward`
v4 FastStore uses the Dart Sass **module system**. `@import` is deprecated
(removed in Dart Sass 3.0). Treat this as a repo-wide pass over every `.scss`
file in the store.
### 4.1 Namespaced mixins & functions
Shared mixins/functions are reached through a **namespaced module**:
```scss
@use "@faststore/ui/src/styles/base/utilities" as u; // FIRST line(s) of the file
[data-fs-foo] {
@include u.media(">=notebook") { ... } // not @include media(...)
top: u.rem(9px); // not rem(9px)
@include u.layout-content; // not @include layout-content
}
```
- All `@use` rules must come **before** every other rule (including CSS
`@import` and selectors). Group them at the very top of the file.
- A bare `@use "<file>";` still emits the loaded file's CSS; only its Sass
*members* become namespaced. Add `as <ns>` only when you need its members.
### 4.2 Top-level `@import` → `@use`
```scss
@import "~pkg/themes/_buttons.scss"; // before
@use "pkg/themes/_buttons.scss"; // after — drop the legacy `~` prefix
```
Plain CSS `@import` (URL ending in `.css`) is **not** deprecated — leave those
as-is.
### 4.3 Nested `@import` must STAY `@import`
A `@use` rule **cannot be nested** inside a selector. The naive fix (hoist the
import to the top level) **breaks CSS-Modules files** (`*.module.scss`):
hoisting `@use ".../Loader/styles.scss"` lifts `[data-fs-loader]` to the top
level and css-loader rejects it — `Selector "[data-fs-loader]" is not pure`.
`@use`-ing several `.../styles.scss` files also collides on the default
namespace `styles` (`There's already a module with namespace "styles"`).
So the rule is:
- **Top-level `@import` → `@use`.**
- **Nested `@import` (inside a selector) → leave as `@import`.** Dart Sass still
supports it (deprecation warning only), and keeping it nested preserves the
local-class scoping that makes the inner selectors "pure". A 100% `@import`
purge is not achievable for CSS-Modules files — and that is fine.
### 4.4 Custom breakpoints → standard FastStore breakpoints
v3 stores override include-media breakpoints by re-declaring a global
`$breakpoints` map (typically in a `custom-mixins.scss`). That global-shadowing
trick is **dead** under the module system: include-media is configured **once**
by `@faststore/ui/.../utilities.scss` (`@use "~include-media" with (...)`) and
cannot be reconfigured.
Fix: drop the non-standard breakpoints from the theme and remap each `media()`
call to the nearest standard FastStore breakpoint
(`phone, phonemid, tablet, notebook, desktop`). Example remaps:
`phonelg → tablet`, `notebooksm → notebook`. Do **not** edit `@faststore/ui`'s
breakpoint map — it is shared, and the store's CMS schema only knows the
standard breakpoints. A leftover `custom-mixins.scss` becomes a thin
`@forward "@faststore/ui/src/styles/base/utilities";`.
### 4.5 "Module already loaded" — dead / duplicate imports
```
This module was already loaded, so it can't be configured using "with".
```
`utilities.scss` does `@use "~include-media" with (...)`; loading it as two
different module instances configures include-media twice. The usual cause is
a dead theme partial (e.g. an `_base.scss` that only `@use`s utilities and
re-declares `$breakpoints` — a no-op in v4) imported through a *different*
specifier than the rest of the store. Fix: drop the dead `@use` — verify the
partial actually contributes CSS/members before keeping it.
### 4.6 Per-file checklist
1. Move/add `@use "@faststore/ui/src/styles/base/utilities" as u;` to the top.
2. `@include media(...)` → `@include u.media(...)`;
`@include layout-content` → `@include u.layout-content`;
the FastStore `rem(...)` function → `u.rem(...)`.
3. Top-level `@import` → `@use` (drop `~`); nested `@import` stays (§4.3).
4. Remap non-standard breakpoints (§4.4); drop dead imports (§4.5).
5. Rebuild and check the Sass output.
---
## 5. GraphQL import migration
`@faststore/graphql-utils` is **deprecated** in v4. The `gql` tag used for
GraphQL documents must be imported from `@faststore/core/api` instead.
Search for all usages in `src/`:
```bash
grep -r "faststore/graphql-utils" src/
```
Replace every occurrence:
```ts
// before
import { gql } from '@faststore/graphql-utils'
// after
import { gql } from '@faststore/core/api'
```
If the grep returns no matches, skip this step.
---
## 6. v3 patches
`patch-package` patches are version-tagged (`@faststore+core+<v3>.patch`) and
will not apply to v4. Inspect each:
- **Debug / instrumentation-only patches** (verbose logging gated on an env
flag, pass-through when off) — move out of `patches/` to a sibling folder
such as `../.patches-disabled-v3/`; nothing to reapply.
- **Functional patches** — re-evaluate whether v4 still needs the fix; if so,
recreate it against the v4 package.
`patch-package` scans `patches/` recursively, so a `patches/.disabled/`
subfolder is still picked up — move stale patches **outside** `patches/`.
---
## 6. Tooling gotchas
- **corepack signature error** for `pnpm` / `yarn`: prefix commands with
`COREPACK_INTEGRITY_KEYS=0` (an outdated corepack can't verify newer
signatures).
- **turbo + nested git worktree**: turbo walks up past a worktree (whose `.git`
is a *file*) and mis-detects the repo root, so a root `turbo build` reports
`0 tasks`. Build the store package directly:
`cd packages/discovery && yarn build`. Normal checkouts are unaffected.
---
## 7. `@vtex/diagnostics-nodejs` optional peers
`@vtex/diagnostics-nodejs` imports instrumentation for server frameworks
FastStore does not use (`@opentelemetry/instrumentation-koa`,
`@opentelemetry/instrumentation-nestjs-core`, `@nestjs/core`, `fastify-plugin`).
They are optional peers and are not installed.
- In `next build` they are harmless warnings — the server bundle externalises
`node_modules`, so the build succeeds.
- In `next dev` they can become fatal `Module not found` errors and 500 the
page.
If `next dev` 500s on this, stub the missing modules to `false` in webpack.
The store's `discovery.config.js` `webpack()` callback is **not** invoked by
v4 core, so the alias must live in `@faststore/core`'s own
`packages/core/next.config.js` `webpack()` callback (only relevant when running
a linked local `faststore` clone — see §9):
```js
config.resolve.alias = {
...config.resolve.alias,
'@opentelemetry/instrumentation-koa': false,
'@opentelemetry/instrumentation-nestjs-core': false,
'@opentelemetry/instrumentation-fastify': false,
'@opentelemetry/instrumentation-express': false,
'@nestjs/core': false,
'fastify-plugin': false,
}
```
> Do **not** make `core/next.config.js` forward `storeConfig.webpack` — that
> applies the store callback to the `instrumentation` compilation and breaks
> the instrumentation hook. Put shared webpack fixes directly in core's
> callback.
---
## 8. Verification
1. `cd packages/discovery && COREPACK_INTEGRITY_KEYS=0 yarn build` — expect
`generate` + GraphQL codegen + `next build` to succeed, routes printed,
`.next` copied. No `@import` *errors* (deprecation *warnings* from nested
imports are expected), no "module already loaded", no "not pure" selectors.
2. `COREPACK_INTEGRITY_KEYS=0 yarn dev` — homepage `200`,
`POST /api/graphql` → `{"data":{"__typename":"Query"}}` `200`, private
routes redirect (`30x`) to login.
3. Spot-check responsive styling (the remapped breakpoints) and any
placeholder-`@extend` buttons.
---
## 9. Optional: local multi-repo testing (never committed)
Needed only while validating the store against **unpublished** `faststore` or
plugin source (e.g. the v4 branches before they are released). Skip entirely
once published v4 versions exist — which is the normal, committed state.
The technique: after a normal `yarn install`, overlay symlinks so the store and
the plugin resolve `@faststore/*` to a local `faststore` monorepo checkout:
- Symlink every `@faststore/*` package (`api cli core components diagnostics
graphql-utils lighthouse sdk ui`) into the store's `node_modules` **and** the
plugin's `node_modules` (otherwise the plugin pulls its own `@faststore/ui`
and you hit the §4.5 duplicate-`utilities.scss` error).
- Repoint the CLI bin: `node_modules/.bin/faststore → ../@faststore/cli/bin/run.js`
(the v4 path is `bin/run.js`, was `bin/run` in v3).
- Dedupe singletons (`graphql`, `react`, `react-dom`) to the single copy the
faststore packages share, or `yarn dev` fails with
`Duplicate "graphql" modules` / React "invalid hook call".
- Use the `link:` protocol for the plugin (`link:` survives `yarn install`;
`yarn link` does not).
Build the `faststore` monorepo first (`pnpm install && pnpm build`). Drive the
symlink overlay from an idempotent, **`postinstall`-safe** script (exits `0`
when sibling checkouts are absent, so deploy/CI is unaffected).
**Before committing**, revert to the deployable state: pin published versions
in `package.json`, remove any `link:` entries / `postinstall` hook / link
script, and `rm -rf node_modules && yarn install` so no symlinks remain.
---
## Appendix — migration checklist
- [ ] Node 24 (`volta.node` must be a full semver e.g. "24.0.2", `experimental.nodeVersion: 24`).
- [ ] `package.json` (root + discovery): `@faststore/cli` + plugin pinned to
published v4 releases; v3 transitive deps removed; `next` **removed**
from dependencies (managed by cli); `graphql ^16.11.0` and
`react-router-dom ^6.24.1` added; `typescript ^5.9.3` in devDependencies.
- [ ] `discovery.config.js`: no `path`/`dotenv` requires; no `webpack()`
callback; `nodeVersion: 24`; `transpilePackages` if needed.
- [ ] Every `.scss`: top-level `@import` → `@use`; nested `@import` kept;
`@include media`/`layout-content` namespaced to `u.*`; non-standard
breakpoints remapped to standard ones; dead theme imports dropped.
- [ ] `custom-mixins.scss` (if present) → thin `@forward` of utilities.
- [ ] v3 `patch-package` patches assessed and stale ones moved out of
`patches/`.
- [ ] `yarn build` and `yarn dev` verified (§8).
- [ ] Local-linking machinery (§9) reverted before committing.
- [ ] CMS type detected via `contentSource` in `discovery.config.js` (§11.1).
- [ ] CMS sync instructions shown to user (§10 next steps): case detected from `discovery.config.js` + `cms/faststore/`; commands presented for manual execution (never run automatically).
- [ ] New CMS fields configured in Admin → Storefront → Headless CMS / Content and pages republished (§11.4 — human step).
- [ ] Tested locally with `yarn dev` — no empty labels/buttons/toasts.
- [ ] Only then: v4 deployed to production + Node.js v24 set in WebOps.
---
## 10. Post-migration summary (mandatory output)
> **MANDATORY prerequisite — do NOT display this summary until `yarn build`
> passes.**
>
> Before showing the summary, run (using Node 24):
> ```bash
> yarn install
> yarn build
> ```
> Fix any build errors first. Only after a successful build should you
> proceed to display the summary below.
After the build passes, display the following two sections.
---
### What was done
A table covering every file touched and the change applied. Adapt rows to
what actually changed; mark items that were not applicable as `—`.
| File | Change | Status |
|------|--------|--------|
| `package.json` | `@faststore/cli` bumped to v4; `next` removed; `graphql`, `react-router-dom` added; `typescript` bumped to `^5.9.3`; `volta.node` set to `24` | ✅ Done |
| `discovery.config.js` | `experimental.nodeVersion` → `24` | ✅ Done |
| `src/**/*.scss` | Top-level `@import` → `@use`; `@include media/layout-content` namespaced to `u.*` | ✅ Done |
| `patches/` | Stale v3 patches assessed / moved | ✅ Done / N/A |
---
### Important next steps
#### 1. CMS sync (run manually in your terminal)
> **Do NOT run `vtex content` commands automatically.** These require
> interactive authentication and may have CLI plugin issues in Homebrew
> environments.
Check `discovery.config.js` and `cms/faststore/` to identify the case,
then present the matching instructions to the user.
**Headless CMS (legacy)** — `contentSource` field absent in `discovery.config.js`:
```bash
vtex login <accountName>
yarn cms-sync
```
> If `cms-sync` errors with `Cannot find module 'vtex'`, run
> `vtex plugins install @vtex/cli-plugin-cms` or `vtex update`.
**Content Platform (CP)** — `contentSource: { type: 'CP' }` present.
Identify the sub-case by inspecting `cms/faststore/`:
| Case | Signal | Commands to run |
|------|--------|-----------------|
| **A** — no custom schemas | No `.jsonc` files, no `components/` folder | Create `cms/faststore/schema.json` with `{ "$base": "vtex.faststore" }`, then `vtex content upload-schema cms/faststore/schema.json` |
| **B** — already split | `cms/faststore/components/*.jsonc` exists | `vtex content generate-schema cms/faststore/components cms/faststore/pages -o cms/faststore/schema.json` then `vtex content upload-schema cms/faststore/schema.json` |
| **C** — legacy format | Only `sections.json` / `content-types.json` | Split first (see §11), then generate + upload |
For the full command listing of each case see §11.
---
#### 2. Fill in new CMS fields and republish
After the CMS sync, v4 exposes new configurable fields that were previously
hardcoded. Configure them in **Admin → Storefront → Headless CMS / Content**
and republish the affected pages:
| Page | Fields to configure |
|------|---------------------|
| **All pages** | `Navbar` → `invalidQuantityToast`, `collapseSearchAriaLabel` |
| **Home** (product shelf) | `ProductCard` / `ProductCardContent` → `buttonLabel`, `outOfStockLabel`, `includeTaxesLabel`, `sponsoredLabel` |
| **PLP** | `Breadcrumb → Fallback label`, `ProductGallery → sortBySelector`, `Filter → FilterSlider / FilterDesktop labels`, `ProductCard / ProductCardContent` |
| **Search** | `SearchInput`, `SearchTop`, `SearchHistory`, `EmptyGallery → labels`, `ProductCard / ProductCardContent` |
| **PDP** | `Breadcrumb → Fallback label`, `ProductDetails → invalidQuantityToast / buyButtonTitle` |
| **Cart** | `EmptyCart → title / buttonLabel` |
Full field reference: [developers.vtex.com → Upgrading FastStore to v4](https://developers.vtex.com/docs/guides/faststore/getting-started-upgrading-faststore-to-v4)
> Do not deploy v4 to production before filling these fields — they render
> blank until configured.
---
#### 3. Update Node.js v24 in WebOps
In VTEX Admin → **Storefront → FastStore WebOps → Settings → Node.js
version** → set to `v24` → Save → trigger a new deploy.
---
#### 4. Check Sass `@import` deprecation warnings
`@import` inside selector blocks emits Dart Sass deprecation warnings.
---
## 11. CMS sync — command reference
This section is a command reference. The agent must **not** run these
commands automatically — always present them to the user to run manually.
### 11.1 Headless CMS (legacy)
```bash
vtex login <accountName>
yarn cms-sync
```
`cms-sync` is safe while v3 is live — it only pushes the schema.
### 11.2 Content Platform — Case A (no custom schemas)
```bash
# 1. Create the minimal schema (if not already present)
# cms/faststore/schema.json content:
# { "$base": "vtex.faststore" }
# (use "vtex.faststore@4.1.0" to pin an explicit version)
vtex login <accountName>
vtex content upload-schema cms/faststore/schema.json
# The local schema.json can be deleted after upload
```
### 11.3 Content Platform — Case B (components already in .jsonc format)
```bash
vtex login <accountName>
vtex content generate-schema cms/faststore/components cms/faststore/pages \
-o cms/faststore/schema.json
vtex content upload-schema cms/faststore/schema.json
```
### 11.4 Content Platform — Case C (legacy sections.json)
```bash
vtex login <accountName>
vtex content split-components -i cms/faststore/sections.json \
-o cms/faststore/components
vtex content split-content-types -i cms/faststore/content-types.json \
-s cms/faststore/sections.json \
-o cms/faststore/pages
vtex content generate-schema cms/faststore/components cms/faststore/pages \
-o cms/faststore/schema.json
vtex content upload-schema cms/faststore/schema.json
```references/graphql-types-queries-and-mutations.md
---
name: faststore-graphql-bff
description: Complete FastStore GraphQL BFF API reference including all built-in root queries, root mutations, and type definitions. Use when querying product, collection, search, cart, session, shipping, or user order data, when extending existing types with new fields, or when looking up available fields on types like StoreProduct, StoreOffer, StoreSession, StoreCart, StoreFacet, and others.
metadata:
author: vtex
version: "1.0"
---
# FastStore GraphQL BFF — API Reference
FastStore exposes a GraphQL BFF (Back-For-Front) layer that proxies between your storefront and VTEX platform APIs.
The full type reference is in [references/REFERENCE.md](references/REFERENCE.md).
## Root Query Fields (Summary)
| Field | Return Type | Description |
| -------------------------------------------------- | ---------------------------- | ------------------------------------ |
| `product(locator)` | `StoreProduct!` | Product details by locator |
| `collection(slug)` | `StoreCollection!` | Collection details by slug |
| `search(first, after, sort, term, selectedFacets)` | `StoreSearchResult!` | Product/facet/suggestion search |
| `allProducts(first, after)` | `StoreProductConnection!` | All products |
| `products(productIds)` | `[StoreProduct!]!` | Products by IDs |
| `allCollections(first, after)` | `StoreCollectionConnection!` | All collections |
| `shipping(items, postalCode, country)` | `ShippingData` | Shipping simulation |
| `sellers(postalCode, country, ...)` | `SellersData` | Available sellers |
| `profile(id)` | `Profile` | Profile information |
| `userOrder(orderId)` | `UserOrderResult` | Order details (auth required) |
| `listUserOrders(...)` | `UserOrderListMinimalResult` | Order list (auth required) |
| `userDetails` | `StoreUserDetails!` | Current user details (auth required) |
| `pickupPoints(geoCoordinates)` | `PickupPoints` | Nearby pickup points |
## Root Mutation Fields (Summary)
| Field | Return Type | Description |
| ---------------------------------- | ------------------ | -------------------------------- |
| `validateCart(cart, session)` | `StoreCart` | Validate/sync cart with platform |
| `validateSession(session, search)` | `StoreSession` | Update web session |
| `subscribeToNewsletter(data)` | `PersonNewsletter` | Newsletter subscription |
| `cancelOrder(data)` | `UserOrderCancel` | Cancel a user order |
## Key Types at a Glance
- **`StoreProduct`** — A VTEX SKU. Contains `name`, `sku`, `slug`, `brand`, `offers`, `image`, `description`, `seo`, `additionalProperty`, `isVariantOf`, and more.
- **`StoreOffer`** — Offer for a product from a seller. Contains `price`, `sellingPrice`, `listPrice`, `availability`, `seller`, `quantity`.
- **`StoreSearchResult`** — Search results with `products`, `facets`, `suggestions`, and `metadata`.
- **`StoreSession`** — Session state: `locale`, `currency`, `country`, `channel`, `person`, `postalCode`.
- **`StoreCart`** — Shopping cart: `order` (with `acceptedOffer` array) and `messages`.
- **`StoreFacetBoolean` / `StoreFacetRange`** — Search facets with keys, labels, and values.
## Runtime shape vs Schema shape
GraphQL responses may differ from the schema definition:
- **Union/interface types** add `__typename` at runtime (e.g., `StoreFacet` → `__typename: "StoreFacetBoolean"` or `"StoreFacetRange"`). The schema field `type: StoreFacetType` may NOT appear in the response if the query uses inline fragments (`... on`) instead of requesting `type` directly.
Common example:
```tsx
// ❌ Wrong — `type` field may not exist at runtime
const booleanFacets = facets.filter((f) => f.type === "BOOLEAN");
// ✅ Correct — use __typename from the GraphQL response
const booleanFacets = facets.filter(
(f) => f.__typename === "StoreFacetBoolean",
);
```
## Using the `gql` tag
The `gql` tag from `@faststore/core/api` is **statically extracted at build time** by the FastStore CLI pipeline. It has specific usage restrictions:
### ✅ Where `gql` works:
- **API extension fragments** in `src/fragments/` (e.g., `ServerProduct.ts`, `ClientProduct.ts`)
- **Third-party mutations/queries** in `src/graphql/thirdParty/`
### ❌ Where `gql` does NOT work:
- **Inside custom section components** for standalone queries against built-in root queries (`search`, `product`, `collection`)
### Why it fails in components:
The FastStore CLI's GraphQL optimization step runs at build time and only processes `gql` tags in specific locations. Using it in a component for a new query will break the build with:
```
"GraphQL was not optimized and TS files were not updated"
```
### ✅ Correct approach for custom sections:
Read data from page context hooks instead of creating new queries:
```tsx
// ❌ Wrong — will break the build
import { gql } from "@faststore/core/api";
export default function MySection() {
const query = gql(`
query MySearch($term: String!) {
search(term: $term) { ... }
}
`);
// This will fail at build time
}
// ✅ Correct — read from page context
import { usePage } from "@faststore/core";
export default function MySection() {
const context = usePage<PLPContext>();
const searchData = context?.data?.search;
// Data is already available from the page query
}
```
See [references/section-overrides-and-custom-sections.md](references/section-overrides-and-custom-sections.md) for more details on creating custom sections that consume data from page context.
## Extending Types
Use `extend type <TypeName>` in `src/graphql/vtex/typeDefs/*.graphql` to add fields to any built-in type:
```graphql
extend type StoreProduct {
customField: String!
}
```
See [extending-graphql-with-custom-resolvers](extending-graphql-with-custom-resolvers.md) for the complete extension guide.
## Sort Options (`StoreSort` enum)
| Value | Description |
| --------------- | ---------------------- |
| `price_desc` | Price: high to low |
| `price_asc` | Price: low to high |
| `orders_desc` | Most orders first |
| `name_desc` | Name: Z to A |
| `name_asc` | Name: A to Z |
| `release_desc` | Newest first |
| `discount_desc` | Biggest discount first |
| `score_desc` | Best score first |
## Full Type Reference
See [references/REFERENCE.md](references/REFERENCE.md) for the complete field-by-field reference for all types.
## API Extensions — GraphQL
FastStore provides two extension mechanisms for GraphQL:
1. **VTEX extensions** (`src/graphql/vtex/`) — extend the existing FastStore API types with new fields. The resolvers have access to the VTEX platform data that comes from the root object.
2. **Third-party extensions** (`src/graphql/thirdParty/`) — define entirely new types, queries, and mutations that call external APIs.
###1 Extending the VTEX Schema (adding fields to existing types)
#### Step 1: Define the new type and extend the existing type
```graphql
# src/graphql/vtex/typeDefs/product.graphql
# Extends the native StoreProduct type with installment data.
# The "extend type" syntax adds fields to an existing FastStore API type.
type Installments {
installmentPaymentSystemName: String!
installmentValue: Float!
installmentInterest: Float!
installmentNumber: Float!
}
extend type StoreProduct {
"""
Retrieve available installments data extending StoreProduct
"""
availableInstallments: [Installments!]!
}
```
#### Step 2: Write the resolver
```ts
// src/graphql/vtex/resolvers/product.ts
// Resolves the "availableInstallments" field added to StoreProduct.
// The `root` parameter contains the raw VTEX catalog data for the product,
// which includes seller information, commercial offers, and installment plans.
import type { StoreProductRoot } from "@faststore/core/api";
// StoreProductRoot: TypeScript type representing the raw VTEX product data
// that FastStore passes to StoreProduct resolvers.
const productResolver = {
StoreProduct: {
availableInstallments: (root: StoreProductRoot) => {
// Access installments from the first seller's commercial offer.
// The VTEX API nests this data under sellers[].commertialOffer.Installments.
// <!-- TODO: "commertialOffer" appears to be a known typo in the VTEX API
// (should be "commercialOffer"). Confirm this is intentional and not a bug. -->
const installments = root.sellers?.[0]?.commertialOffer?.Installments;
if (!installments.length) {
return [];
}
// Map the raw VTEX installment shape to our GraphQL schema shape.
return installments.map((installment) => ({
installmentPaymentSystemName: installment.PaymentSystemName,
installmentValue: installment.Value,
installmentInterest: installment.InterestRate,
installmentNumber: installment.NumberOfInstallments,
}));
},
},
};
export default productResolver;
```
#### Step 3: Export from the resolver index
```ts
// src/graphql/vtex/resolvers/index.ts
// Aggregates all VTEX API extension resolvers into a single export.
// FastStore CLI reads this file to merge resolvers into the API.
import { default as StoreProductResolver } from "./product";
const resolvers = {
...StoreProductResolver,
};
export default resolvers;
```
#### Step 4: Add fragments to include the new fields in page queries
FastStore uses a fragment-based system to extend the data fetched on each page. Fragment filenames must match the query they extend.
```ts
// src/fragments/ServerProduct.ts
// Server-side fragment: included in the initial server-rendered HTML.
// The filename "ServerProduct" tells FastStore to merge this fragment
// into the server-side product query for the PDP.
import { gql } from "@faststore/core/api";
export const fragment = gql(`
fragment ServerProduct on Query {
product(locator: $locator) {
availableInstallments {
installmentPaymentSystemName
installmentValue
installmentInterest
installmentNumber
}
}
}
`);
// Docs: https://developers.vtex.com/docs/guides/faststore/api-extensions-extending-queries-using-fragments
```
```ts
// src/fragments/ClientProduct.ts
// Client-side fragment: used for client-side data fetching (e.g., SWR revalidation).
// Must include the same fields as the server fragment so client and server data match.
import { gql } from "@faststore/core/api";
export const fragment = gql(`
fragment ClientProduct on Query {
product(locator: $locator) {
availableInstallments {
installmentPaymentSystemName
installmentValue
installmentInterest
installmentNumber
}
}
}
`);
```
#### Fragment naming convention
| Filename | Extends query for |
| ------------------------------- | -------------------------------- |
| `ServerProduct.ts` | Server-side PDP query |
| `ClientProduct.ts` | Client-side PDP query |
| `ClientProductGallery.ts` | Client-side PLP query |
| `ClientManyProducts.ts` | |
| `ClientSearchSuggestions.ts` | Search autocomplete query |
| `ClientShippingSimulation.ts` | Shipping simulation query |
| `ClientTopSearchSuggestions.ts` | Top search query |
| `ClientCollectionPage.ts` | Client-side PLP/collection query |
| `ClientTopSearchSuggestions.ts` | Top search suggestions query |
| `ServerCollectionPage.ts` | Server-side PLP/collection query |
| `ServerProduct.ts` | Server-side product query |
###2 Third-Party Extensions (new types and mutations)
#### Step 1: Define new GraphQL types
```graphql
# src/graphql/thirdParty/typeDefs/contactForm.graphql
# Defines a completely new mutation for submitting a contact form.
# This is NOT extending an existing type — it's a new root Mutation field.
type ContactFormResponse {
message: String!
}
input ContactFormInput {
name: String!
email: String!
subject: String!
message: String!
}
type Mutation {
submitContactForm(input: ContactFormInput!): ContactFormResponse
}
```
#### Step 2: Write the resolver
```ts
// src/graphql/thirdParty/resolvers/contactForm.ts
// Server-side resolver for the submitContactForm mutation.
// This runs on the Node.js server, so it can make authenticated API calls
// that shouldn't be exposed to the browser.
type SubmitContactFormData = {
input: {
name: string;
email: string;
subject?: string;
message: string;
};
};
const contactFormResolver = {
Mutation: {
submitContactForm: async (_: never, data: SubmitContactFormData) => {
const { input } = data;
try {
// POST to the VTEX Master Data API (Data Entities).
// This is a server-side call — the API key/credentials are managed
// by the VTEX platform, not exposed to the client.
// <!-- TODO: The URL is hardcoded to "playground.vtexcommercestable.com.br".
// In production, this should be dynamic based on discovery.config.js api settings.
// Confirm if there's a recommended way to access the store config from resolvers. -->
const response = await fetch(
"https://playground.vtexcommercestable.com.br/api/dataentities/ContactForm/documents?_schema=contactForm",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(input),
},
);
if (!response.ok) {
throw new Error("Error while sending the message");
}
return { message: "Your message was sent successfully!" };
} catch (error) {
return { message: error };
}
},
},
};
export default contactFormResolver;
```
#### Step 3: Export from the resolver index
```ts
// src/graphql/thirdParty/resolvers/index.ts
// Aggregates all third-party resolvers.
// FastStore CLI reads this file to register them with the GraphQL server.
import contactFormResolver from "./contactForm";
const resolvers = {
...contactFormResolver,
};
export default resolvers;
```
###3 Consuming extended data in components
```tsx
// src/components/BuyButtonWithDetails/BuyButtonWithDetails.tsx
// Custom BuyButton that displays installment information fetched via API extensions.
import { usePDP } from "@faststore/core";
// usePDP: Hook that provides all PDP data, including extended fields from fragments.
// The data shape includes both native FastStore fields and your custom extensions.
import { Button as UIButton, ButtonProps } from "@faststore/ui";
import { priceFormatter } from "../../utils/priceFormatter";
import styles from "./buy-button-with-details.module.scss";
export function BuyButtonWithDetails(props: ButtonProps) {
// usePDP() returns the full PDP context including data from ServerProduct/ClientProduct fragments.
const context = usePDP();
// Access the custom "availableInstallments" field we added via the VTEX API extension.
const installment = context?.data?.product?.availableInstallments[0];
const interestFree = installment.installmentInterest === 0 ?? false;
return (
<section className={styles.buyButtonWithDetails}>
{interestFree && (
<span>
{`${installment.installmentNumber} interest-free installment(s)`}
<br />
{`of ${priceFormatter(installment.installmentValue)} with ${
installment.installmentPaymentSystemName
}`}
</span>
)}
{/* Spread native ButtonProps so the component remains compatible
with the slot it replaces in ProductDetailsSection. */}
<UIButton {...props} variant="primary">
Buy Button
</UIButton>
</section>
);
}
export default BuyButtonWithDetails;
```
---
## Built-in VTEX Type Reference
These are the types already provided by the FastStore GraphQL API. Use `extend type <TypeName>` in your VTEX extension schemas to add fields to any of them.
### Root Query
The top-level `Query` type exposes these fields:
| Field | Arguments | Return Type | Description |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | ---------------------------------------------------------------------- |
| `product` | `locator: [IStoreSelectedFacet!]!` | `StoreProduct!` | Returns the details of a product based on the specified locator. |
| `collection` | `slug: String!` | `StoreCollection!` | Returns the details of a collection based on the collection slug. |
| `search` | `first: Int!`, `after: String`, `sort: StoreSort`, `term: String`, `selectedFacets: [IStoreSelectedFacet!]`, `sponsoredCount: Int` | `StoreSearchResult!` | Returns the result of a product, facet, or suggestion search. |
| `allProducts` | `first: Int!`, `after: String` | `StoreProductConnection!` | Returns information about all products. |
| `products` | `productIds: [String!]!` | `[StoreProduct!]!` | Returns information about selected products. |
| `allCollections` | `first: Int!`, `after: String` | `StoreCollectionConnection!` | Returns information about all collections. |
| `shipping` | `items: [IShippingItem!]!`, `postalCode: String!`, `country: String!` | `ShippingData` | Returns information about shipping simulation. |
| `redirect` | `term: String`, `selectedFacets: [IStoreSelectedFacet!]` | `StoreRedirect` | Returns if there's a redirect for a search. |
| `sellers` | `postalCode: String`, `geoCoordinates: IGeoCoordinates`, `country: String!`, `salesChannel: String` | `SellersData` | Returns a list of sellers available for a specific localization. |
| `profile` | `id: String!` | `Profile` | Returns information about the profile. |
| `productCount` | `term: String` | `ProductCountResult` | Returns the total product count based on location. |
| `userOrder` | `orderId: String!` | `UserOrderResult` | Returns the details of a user order. Requires auth. |
| `listUserOrders` | `page: Int`, `perPage: Int`, `status: [String]`, `dateInitial: String`, `dateFinal: String`, `text: String`, `clientEmail: String`, `pendingMyApproval: Boolean` | `UserOrderListMinimalResult` | Returns the list of orders the user can view. Requires auth. |
| `userDetails` | — | `StoreUserDetails!` | Returns the current user details. Requires auth. |
| `accountProfile` | — | `StoreAccountProfile!` | Returns the account profile for the authenticated user. Requires auth. |
| `validateUser` | — | `ValidateUserData` | Returns information about user validation. Requires auth. |
| `pickupPoints` | `geoCoordinates: IStoreGeoCoordinates` | `PickupPoints` | Returns a list of pickup points near the given coordinates. |
### Root Mutation
| Field | Arguments | Return Type | Description |
| --------------------------- | --------------------------------------------- | ----------------------------------- | ------------------------------------------------------------- |
| `validateCart` | `cart: IStoreCart!`, `session: IStoreSession` | `StoreCart` | Checks for changes between the UI cart and the platform cart. |
| `validateSession` | `session: IStoreSession!`, `search: String!` | `StoreSession` | Updates a web session with the specified values. |
| `subscribeToNewsletter` | `data: IPersonNewsletter!` | `PersonNewsletter` | Subscribes a new person to the newsletter list. |
| `cancelOrder` | `data: IUserOrderCancel!` | `UserOrderCancel` | Cancels a user order. |
| `processOrderAuthorization` | `data: IProcessOrderAuthorization!` | `ProcessOrderAuthorizationResponse` | Process order authorization (approve/reject). |
---
### StoreProduct
The main product type. Equivalent to a VTEX SKU.
| Field | Type | Description |
| ----------------------- | ------------------------ | ---------------------------------------------------------------------- |
| `seo` | `StoreSeo!` | Meta tag data. |
| `breadcrumbList` | `StoreBreadcrumbList!` | Chain of linked web pages ending with the current page. |
| `slug` | `String!` | Corresponding collection URL slug. |
| `name` | `String!` | Product name. |
| `productID` | `String!` | Product ID (e.g., ISBN or similar global IDs). |
| `brand` | `StoreBrand!` | Product brand. |
| `description` | `String!` | Product description. |
| `image` | `[StoreImage!]!` | Array of images. Accepts `context: String` and `limit: Int` arguments. |
| `offers` | `StoreAggregateOffer!` | Aggregate offer information. |
| `sku` | `String!` | Stock Keeping Unit (merchant-specific ID). |
| `gtin` | `String!` | Global Trade Item Number. |
| `review` | `[StoreReview!]!` | Array with review information. |
| `aggregateRating` | `StoreAggregateRating!` | Aggregate ratings data. |
| `isVariantOf` | `StoreProductGroup!` | Indicates the product group related to this product. |
| `additionalProperty` | `[StorePropertyValue!]!` | Array of additional properties. |
| `releaseDate` | `String!` | The product's release date (ISO 8601). |
| `unitMultiplier` | `Float` | SKU unit multiplier. |
| `advertisement` | `Advertisement` | Advertisement information about the product. |
| `hasSpecifications` | `Boolean` | Indicates whether the product has specifications. |
| `skuSpecifications` | `[SkuSpecification!]!` | The specifications of a product. |
| `specificationGroups` | `[SpecificationGroup!]!` | The specifications of a group of SKUs. |
| `deliveryPromiseBadges` | `[DeliveryPromiseBadge]` | Delivery promise product badges. |
### StoreProductGroup
Product groups are catalog entities that may contain variants. Equivalent to VTEX Products.
| Field | Type | Description |
| -------------------- | ------------------------ | -------------------------------------------------------------- |
| `hasVariant` | `[StoreProduct!]!` | Array of variants related to the product group. |
| `productGroupID` | `String!` | Product group ID. |
| `name` | `String!` | Product group name. |
| `additionalProperty` | `[StorePropertyValue!]!` | Array of additional properties. |
| `skuVariants` | `SkuVariants` | Data structures for handling different SKU variant properties. |
### StoreOffer
Offer information for a product.
| Field | Type | Description |
| -------------------- | -------------------- | ----------------------------------------------------------- |
| `listPrice` | `Float!` | Displayed as the "from" price in promotions. |
| `listPriceWithTaxes` | `Float!` | List price with current taxes. |
| `sellingPrice` | `Float!` | Computed price before applying coupons, taxes, or benefits. |
| `priceCurrency` | `String!` | ISO code of the currency used for the offer prices. |
| `price` | `Float!` | Also known as spot price. |
| `priceWithTaxes` | `Float!` | Spot price with taxes. |
| `priceValidUntil` | `String!` | Next date when price is scheduled to change. |
| `itemCondition` | `String!` | Offer item condition. |
| `availability` | `String!` | Offer item availability. |
| `seller` | `StoreOrganization!` | Seller responsible for the offer. |
| `itemOffered` | `StoreProduct!` | Information on the item being offered. |
| `quantity` | `Int!` | Number of items offered. |
### StoreAggregateOffer
Aggregate offer information for a given SKU across multiple sellers.
| Field | Type | Description |
| ------------------- | ---------------- | --------------------------------------------------- |
| `highPrice` | `Float!` | Highest price among all sellers. |
| `lowPrice` | `Float!` | Lowest price among all sellers. |
| `lowPriceWithTaxes` | `Float!` | Lowest price among all sellers with current taxes. |
| `offerCount` | `Int!` | Number of sellers selling this SKU. |
| `priceCurrency` | `String!` | ISO code of the currency used for the offer prices. |
| `offers` | `[StoreOffer!]!` | Array with information on each available offer. |
### StoreCollection
Product collection information.
| Field | Type | Description |
| ---------------- | ---------------------- | -------------------------------------------------------------------------------------------- |
| `seo` | `StoreSeo!` | Meta tag data. |
| `breadcrumbList` | `StoreBreadcrumbList!` | Breadcrumb list for navigation. |
| `meta` | `StoreCollectionMeta!` | Collection meta information (selected facets). |
| `id` | `ID!` | Collection ID. |
| `slug` | `String!` | Collection URL slug. |
| `type` | `StoreCollectionType!` | Collection type (`Department`, `Category`, `SubCategory`, `Brand`, `Cluster`, `Collection`). |
### StoreSearchResult
Search result data.
| Field | Type | Description |
| ------------- | ------------------------- | -------------------------------------------------------------- |
| `products` | `StoreProductConnection!` | Search result products (with pagination). |
| `facets` | `[StoreFacet!]!` | Array of search result facets. |
| `suggestions` | `StoreSuggestions!` | Search result suggestions. |
| `metadata` | `SearchMetadata` | Search result metadata (misspelling, fuzzy, logical operator). |
### StoreSeo
Search Engine Optimization tags data.
| Field | Type | Description |
| --------------- | --------- | ------------------- |
| `title` | `String!` | Title tag. |
| `titleTemplate` | `String!` | Title template tag. |
| `description` | `String!` | Description tag. |
| `canonical` | `String!` | Canonical tag. |
### StoreBreadcrumbList
Breadcrumb navigation list.
| Field | Type | Description |
| ----------------- | ------------------- | ---------------------------------- |
| `itemListElement` | `[StoreListItem!]!` | Array with breadcrumb elements. |
| `numberOfItems` | `Int!` | Number of breadcrumbs in the list. |
### StoreListItem
Single breadcrumb item.
| Field | Type | Description |
| ---------- | --------- | --------------------------------- |
| `item` | `String!` | List item value. |
| `name` | `String!` | Name of the list item. |
| `position` | `Int!` | Position of the item in the list. |
### StoreBrand
| Field | Type | Description |
| ------ | --------- | ----------- |
| `name` | `String!` | Brand name. |
### StoreImage
| Field | Type | Description |
| --------------- | --------- | -------------------- |
| `url` | `String!` | Image URL. |
| `alternateName` | `String!` | Alias for the image. |
### StorePropertyValue
Properties associated with products and product groups.
| Field | Type | Description |
| ---------------- | ----------------- | ------------------------------------------------------- |
| `propertyID` | `String!` | Property ID. |
| `value` | `ObjectOrString!` | Property value (may be a string or stringified object). |
| `name` | `String!` | Property name. |
| `valueReference` | `ObjectOrString!` | Specifies the nature of the value. |
### StoreReview
| Field | Type | Description |
| -------------- | -------------------- | -------------------------- |
| `reviewRating` | `StoreReviewRating!` | Review rating information. |
| `author` | `StoreAuthor!` | Review author. |
### StoreReviewRating
| Field | Type | Description |
| ------------- | -------- | ------------------ |
| `ratingValue` | `Float!` | Rating value. |
| `bestRating` | `Float!` | Best rating value. |
### StoreAggregateRating
| Field | Type | Description |
| ------------- | -------- | ------------------------------ |
| `ratingValue` | `Float!` | Value of the aggregate rating. |
| `reviewCount` | `Int!` | Total number of ratings. |
### StoreOrganization (Seller)
| Field | Type | Description |
| ------------ | --------- | ------------------------- |
| `identifier` | `String!` | Organization / Seller ID. |
### StoreSession
Session information.
| Field | Type | Description |
| ---------------- | --------------------- | ------------------------ |
| `locale` | `String!` | Session locale. |
| `currency` | `StoreCurrency!` | Session currency. |
| `country` | `String!` | Session country. |
| `channel` | `String` | Session channel. |
| `deliveryMode` | `StoreDeliveryMode` | Session delivery mode. |
| `addressType` | `String` | Session address type. |
| `city` | `String` | Session city. |
| `postalCode` | `String` | Session postal code. |
| `geoCoordinates` | `StoreGeoCoordinates` | Session geo coordinates. |
| `person` | `StorePerson` | Session person. |
| `b2b` | `StoreB2B` | B2B information. |
| `marketingData` | `StoreMarketingData` | Marketing information. |
| `refreshAfter` | `String` | Refresh token expiry. |
### StorePerson
Client profile data.
| Field | Type | Description |
| ------------ | --------- | ------------------ |
| `id` | `String!` | Client ID. |
| `email` | `String!` | Client email. |
| `givenName` | `String!` | Client first name. |
| `familyName` | `String!` | Client last name. |
### StoreCurrency
| Field | Type | Description |
| -------- | --------- | ---------------------------- |
| `code` | `String!` | Currency code (e.g., `USD`). |
| `symbol` | `String!` | Currency symbol (e.g., `$`). |
### StoreCart
Shopping cart information.
| Field | Type | Description |
| ---------- | ---------------------- | ------------------------------- |
| `order` | `StoreOrder!` | Order information. |
| `messages` | `[StoreCartMessage!]!` | List of shopping cart messages. |
### StoreOrder
| Field | Type | Description |
| ----------------- | ---------------- | --------------------------------------------------------- |
| `orderNumber` | `String!` | ID of the order in VTEX Order Management. |
| `acceptedOffer` | `[StoreOffer!]!` | Array with information on each accepted offer. |
| `shouldSplitItem` | `Boolean` | Indicates whether items with attachments should be split. |
### StorePageInfo
Pagination information returned in connection queries.
| Field | Type | Description |
| ----------------- | ---------- | ----------------------------------------------------------- |
| `hasNextPage` | `Boolean!` | Whether there is at least one more page after the current. |
| `hasPreviousPage` | `Boolean!` | Whether there is at least one more page before the current. |
| `startCursor` | `String!` | Cursor corresponding to the first possible item. |
| `endCursor` | `String!` | Cursor corresponding to the last possible item. |
| `totalCount` | `Int!` | Total number of items (not pages). |
### SkuVariants
Variant handling data structures.
| Field | Type | Description |
| --------------------- | ------------------- | ----------------------------------------------------------------------------------------------------- |
| `activeVariations` | `ActiveVariations` | SKU property values for the current SKU. |
| `allVariantsByName` | `VariantsByName` | All available options for each SKU variant property, indexed by name. |
| `slugsMap` | `SlugsMap` | Maps property value combinations to their respective SKU slug. Accepts `dominantVariantName: String`. |
| `availableVariations` | `FormattedVariants` | Available options for each varying SKU property. Accepts `dominantVariantName: String`. |
| `allVariantProducts` | `[StoreProduct!]` | All available variant products. |
### SkuSpecification
| Field | Type | Description |
| -------- | --------------------------- | ----------------------------- |
| `field` | `SKUSpecificationField!` | Specification field metadata. |
| `values` | `[SKUSpecificationValue!]!` | Specification values. |
### SpecificationGroup
| Field | Type | Description |
| ---------------- | ------------------- | ----------------------------- |
| `name` | `String!` | Group name. |
| `originalName` | `String!` | Original group name. |
| `specifications` | `[Specification!]!` | Specifications in this group. |
### ShippingData
Shipping simulation information.
| Field | Type | Description |
| --------------- | ----------------- | ------------------------ |
| `items` | `[LogisticsItem]` | List of logistics items. |
| `logisticsInfo` | `[LogisticsInfo]` | List of logistics info. |
| `messages` | `[MessageInfo]` | List of messages. |
| `address` | `Address` | Address information. |
### StoreFacetBoolean
Search facet with boolean values.
| Field | Type | Description |
| -------- | ---------------------------- | ------------------------------------------- |
| `key` | `String!` | Facet key. |
| `label` | `String!` | Facet label. |
| `values` | `[StoreFacetValueBoolean!]!` | Array with information on each facet value. |
### StoreFacetRange
Search facet with range values.
| Field | Type | Description |
| ------- | ----------------------- | -------------------------- |
| `key` | `String!` | Facet key. |
| `label` | `String!` | Facet label. |
| `min` | `StoreFacetValueRange!` | Minimum facet range value. |
| `max` | `StoreFacetValueRange!` | Maximum facet range value. |
### StoreMarketingData
| Field | Type | Description |
| -------------- | -------- | ----------------------- |
| `utmCampaign` | `String` | UTM campaign parameter. |
| `utmMedium` | `String` | UTM medium parameter. |
| `utmSource` | `String` | UTM source parameter. |
| `utmiCampaign` | `String` | Internal UTM campaign. |
| `utmiPart` | `String` | Internal UTM part. |
| `utmiPage` | `String` | Internal UTM page. |
### Enums
#### StoreSort
Product search results sorting options.
| Value | Description |
| --------------- | ------------------------------------------ |
| `price_desc` | Sort by price, highest to lowest. |
| `price_asc` | Sort by price, lowest to highest. |
| `orders_desc` | Sort by orders, highest to lowest. |
| `name_desc` | Sort by name, reverse alphabetical. |
| `name_asc` | Sort by name, alphabetical. |
| `release_desc` | Sort by release date, newest first. |
| `discount_desc` | Sort by discount value, highest to lowest. |
| `score_desc` | Sort by product score, highest to lowest. |
#### StoreCollectionType
| Value | Description |
| ------------- | --------------------------------------- |
| `Department` | First level of product categorization. |
| `Category` | Second level of product categorization. |
| `SubCategory` | Third level of product categorization. |
| `Brand` | Product brand. |
| `Cluster` | Product cluster. |
| `Collection` | Product collection. |
#### StoreStatus
Shopping cart message status.
| Value | Description |
| --------- | ---------------------- |
| `INFO` | Informational message. |
| `WARNING` | Warning message. |
| `ERROR` | Error message. |
references/injecting-head-scripts-and-meta-tags.md
---
name: faststore-third-party-scripts
description: How to inject third-party scripts, meta tags, and head content into a FastStore storefront. Use when adding external scripts (pixel tags, chat widgets, A/B testing tools), site verification meta tags, Google Tag Manager snippets, or any other content that needs to be in the HTML document head.
metadata:
author: vtex
version: "1.0"
---
# FastStore Third-Party Scripts
## Overview
The file `src/scripts/ThirdPartyScripts.tsx` exports a component that gets automatically injected into the document `<head>` by FastStore Core.
Scripts are injected in a worker thread **asynchronously** using the `@builder.io/partytown` library to prevent blocking the main render thread.
## File Location
```
src/scripts/ThirdPartyScripts.tsx
```
This file is automatically picked up by FastStore CLI and injected into the document head during build. You do not need to import or reference it manually.
## Usage
Export a default React component that returns any JSX valid inside `<head>`:
```tsx
// src/scripts/ThirdPartyScripts.tsx
const ThirdPartyScripts = () => {
return (
<>
{/* Site verification meta tag */}
<meta
name="google-site-verification"
content="your-verification-token-here"
/>
{/* Custom script */}
<script
dangerouslySetInnerHTML={{
__html: `
window.myThirdPartyLib = window.myThirdPartyLib || {};
`,
}}
/>
</>
);
};
export default ThirdPartyScripts;
```
## Common Use Cases
### Site Verification Tag
```tsx
const ThirdPartyScripts = () => (
<meta
name="google-site-verification"
content="xRwnzq5B91_3hXsAxoKXfxRwMWk2wsaNwInIjiibTx0"
/>
);
export default ThirdPartyScripts;
```
### Multiple Head Elements
```tsx
const ThirdPartyScripts = () => (
<>
<meta name="google-site-verification" content="..." />
<meta name="facebook-domain-verification" content="..." />
<link rel="preconnect" href="https://fonts.googleapis.com" />
</>
);
export default ThirdPartyScripts;
```
## Notes
- **Google Tag Manager** does not need to be added here — it is configured via `discovery.config.js` using `analytics.gtmContainerId` and injected automatically by FastStore Core.
- The component renders into `<head>`, so only elements valid in `<head>` should be returned.
- Scripts run in a Partytown worker thread, which means they are sandboxed from the main thread for performance. Ensure any third-party scripts you add are compatible with this approach.
references/native-sections-and-overridable-slots.md
---
name: faststore-sections
description: Complete catalog of all built-in FastStore global sections and their overridable component slots. Use when looking up which native sections are available, which inner components (slots) within a section can be overridden via getOverriddenSection, or when deciding whether to override an existing section or create a new one.
metadata:
author: vtex
version: "1.0"
---
# FastStore Global Sections Catalog
These sections are included by default in every FastStore project. Use `getOverriddenSection` from `@faststore/core` to override any of their inner component slots.
See [section-overrides-and-custom-sections](section-overrides-and-custom-sections.md) for override implementation patterns.
## Available Global Sections
- Alert
- BannerText
- Breadcrumb
- CrossSellingShelf
- EmptyState
- Hero
- Navbar
- Newsletter
- ProductDetails
- ProductGallery
- ProductShelf
- RegionBar
- Search
- Footer
- Incentives
- ProductTiles
- Children
- BannerNewsletter
- CartSidebar
- RegionModal
- RegionPopover
## Sections and Their Overridable Component Slots
### Alert
- `Alert`
- `Icon`
### BannerText
- `BannerText`
- `BannerTextContent`
### Breadcrumb
- `Breadcrumb`
- `Icon`
### CrossSellingShelf
- `ProductShelf`
- `__experimentalCarousel`
- `__experimentalProductCard`
### EmptyState
- `EmptyState`
### Hero
- `Hero`
- `HeroImage`
- `HeroHeader`
### Navbar
- `Navbar`
- `NavbarLinks`
- `NavbarLinksList`
- `NavbarSlider`
- `NavbarSliderHeader`
- `NavbarSliderContent`
- `NavbarSliderFooter`
- `NavbarHeader`
- `NavbarRow`
- `NavbarButtons`
- `IconButton`
- `_experimentalButtonSignIn`
- `__experimentalSKUMatrixSidebar`
### Newsletter
- `Button`
- `HeaderIcon`
- `InputFieldEmail`
- `InputFieldName`
- `Newsletter`
- `NewsletterAddendum`
- `NewsletterContent`
- `NewsletterForm`
- `NewsletterHeader`
- `ToastIconError`
- `ToastIconSuccess`
### ProductDetails
- `ProductTitle`
- `DiscountBadge`
- `BuyButton`
- `Icon`
- `ProductPrice`
- `QuantitySelector`
- `SkuSelector`
- `ShippingSimulation`
- `ImageGallery`
- `ImageGalleryViewer`
- `SKUMatrix`
- `SKUMatrixTrigger`
- `SKUMatrixSidebar`
- `__experimentalImageGalleryImage`
- `__experimentalImageGallery`
- `__experimentalShippingSimulation`
- `__experimentalSKUMatrixSidebar`
- `__experimentalNotAvailableButton`
- `__experimentalProductDescription`
- `__experimentalProductDetailsSettings`
### ProductGallery
- `MobileFilterButton`
- `FilterIcon`
- `PrevIcon`
- `ResultsCountSkeleton`
- `SortSkeleton`
- `FilterButtonSkeleton`
- `ToggleField`
- `ProductComparison`
- `ProductComparisonSidebar`
- `ProductComparisonToolbar`
- `LinkButtonPrev`
- `LinkButtonNext`
- `__experimentalFilterDesktop`
- `__experimentalFilterSlider`
- `__experimentalProductCard`
- `__experimentalEmptyGallery`
- `__experimentalProductComparisonSidebar`
> **Note:** Overriding `__experimentalFilterDesktop` or `__experimentalFilterSlider` makes your component fully responsible for the filter/search feature implementation.
### ProductShelf
- `ProductShelf`
- `__experimentalCarousel`
- `__experimentalProductCard`
### RegionBar
- `RegionBar`
- `LocationIcon`
- `ButtonIcon`
- `FilterButtonIcon`
references/project-structure-routes-and-config.md
---
name: faststore-architecture
description: FastStore project structure, routes, CLI build pipeline, store configuration (discovery.config.js), and naming conventions. Use when understanding how a FastStore project is organized, how the @faststore/cli works, what files to create or modify, how routing works, or how to configure store settings like SEO, API, session, and analytics.
metadata:
author: vtex
version: "1.0"
---
# FastStore Architecture Reference
FastStore is an open-source framework by VTEX for high-performance e-commerce. The architecture follows a **thin customization layer** pattern:
- **FastStore Core** (`@faststore/core`) owns pages, routing, data-fetching, and native sections. It runs a Next.js app internally.
- **The storefront repository** (`src/`) contains _only_ the delta — overrides, new sections, API extensions, theme tokens, and CMS schemas. Never touch Next.js pages or routing directly.
- **`@faststore/cli`** orchestrates everything: it generates a `.faststore/` directory (gitignored) that merges core with your customizations, then delegates to Next.js for build/dev/start.
```
┌──────────────────────────────────────────────────────┐
│ Developer repo (src/) │
│ ┌──────────┐ ┌─────────────┐ ┌───────────────────┐ │
│ │ Sections │ │ GraphQL │ │ Themes / Styles │ │
│ │ Overrides│ │ Extensions │ │ (SCSS + tokens) │ │
│ └────┬─────┘ └──────┬──────┘ └────────┬──────────┘ │
│ │ │ │ │
│ ┌────▼──────────────▼─────────────────▼──────────┐ │
│ │ @faststore/cli (build) │ │
│ │ merges into .faststore/ │ │
│ └────────────────────┬───────────────────────────┘ │
│ │ │
│ ┌────────────────────▼───────────────────────────┐ │
│ │ @faststore/core (Next.js app, pages, │ │
│ │ routing, data fetching, native sections) │ │
│ └────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
```
## Project Routes
| Route | URL |
| -------------------------- | ----------------------------- |
| Home (landing page) | `{host}/` |
| PLP — Product Listing Page | `{host}/s` or `{host}/{slug}` |
| PDP — Product Details Page | `{host}/{slug}/p` |
| Error Page | `{host}/500` |
| Not Found | `{host}/404` |
| Login | `{host}/login` |
| Checkout | `{host}/checkout` |
_Slug = product name identifier._
## Project Structure
```
playground.store/
├── cms/
│ └── faststore/
│ └── components/ # Sections definitions in `cms_component__<sectioName>.jsonc` files
│ └── pages/ # new pages definition
│ └── schema.json # CMS final schema definition
├── src/
│ ├── components/
│ │ ├── index.tsx # ** Section registry — maps names to components **
│ │ ├── BuyButtonWithDetails/ # Custom component (used inside a section override)
│ │ ├── ContactForm/ # Standalone new section (not an override)
│ │ └── sections/ # Section-level overrides and new sections
│ ├── fragments/ # GraphQL fragments to extend core queries
│ │ ├── ClientProduct.ts
│ │ └── ServerProduct.ts
│ ├── graphql/
│ │ ├── vtex/ # Extensions to VTEX/FastStore API schema
│ │ │ ├── typeDefs/
│ │ │ └── resolvers/
│ │ └── thirdParty/ # Entirely new schemas (third-party APIs)
│ │ ├── typeDefs/
│ │ └── resolvers/
│ ├── scripts/
│ │ └── ThirdPartyScripts.tsx # Injected into <head>
│ ├── themes/
│ │ └── custom-theme.scss # Design token overrides
│ └── utils/
│ └── priceFormatter.ts
├── discovery.config.js # ** Main store config **
├── cypress.config.ts
├── vtex.env
├── vercel.json
├── package.json
├── tsconfig.json
└── yarn.lock
```
### Key Directories
| Directory | Purpose |
| -------------------------- | ------------------------------------------------------------------------------------------------- |
| `src/components/` | All custom UI — overrides, new sections, sub-components. `index.tsx` is the **section registry**. |
| `src/components/sections/` | Convention for section-level components (each gets its own folder). |
| `src/fragments/` | GraphQL fragments extending FastStore core queries (e.g., add fields to PDP). |
| `src/graphql/vtex/` | Schema extensions and resolvers augmenting the existing FastStore API. |
| `src/graphql/thirdParty/` | Entirely new GraphQL types/mutations for external APIs. |
| `src/themes/` | SCSS files with CSS custom property overrides (design tokens). |
| `src/scripts/` | Third-party script injection. |
| `cms/faststore/` | JSON schemas defining CMS content editor fields. |
| `.faststore/` | **Generated, gitignored** — recreated on every build. Never edit directly. |
## Store Configuration — `discovery.config.js`
This is the central configuration file. FastStore CLI reads it to set up the entire app.
```js
module.exports = {
seo: {
title: "FastStore Playground",
description: "A fast and performant store framework",
titleTemplate: "%s | Playground",
author: "FastStore",
},
theme: "custom-theme", // Must match a filename in src/themes/
platform: "vtex",
api: {
storeId: "playground", // VTEX account name
workspace: "master",
environment: "vtexcommercestable",
hideUnavailableItems: true,
incrementAddress: false,
},
session: {
currency: { code: "BRL", symbol: "R$" },
locale: "pt-BR",
channel: '{"salesChannel":1,"regionId":""}',
country: "BRA",
// ...other session defaults
},
storeUrl: "https://playground.vtex.app",
checkoutUrl: "https://secure.vtexfaststore.com/checkout",
loginUrl: "https://secure.vtexfaststore.com/api/io/login",
analytics: {
gtmContainerId: "GTM-1234567",
},
vtexHeadlessCms: {
webhookUrls: [
"https://playground.myvtex.com/cms-releases/webhook-releases",
],
},
};
```
## CLI Scripts
Example `package.json` scripts (many stores match this shape):
```json
{
"scripts": {
"dev": "faststore dev",
"build": "faststore build",
"start": "faststore start",
"cms-sync": "faststore cms-sync",
"test": "faststore test"
}
}
```
**Headless CMS schema:** Treat **`cms-sync` / `faststore cms-sync`** as **legacy** for **publishing or refreshing** the **Headless CMS** schema. The current flow is **`vtex content generate-schema`** and **`vtex content upload-schema`** (global **VTEX CLI**, `vtex` — not `npx vtex`). See [cms-schema-and-section-registration.md](cms-schema-and-section-registration.md) and the storefront [skill.md](../skill.md).
### What `faststore build` / `faststore dev` Does
1. Reads `discovery.config.js`
2. Generates `.faststore/` (deleted and recreated each run)
3. Copies/merges `src/` into `.faststore/src/customizations/`:
- `src/components/index.tsx` → `.faststore/src/customizations/components/index.tsx`
- `src/themes/*.scss` → injected into global stylesheet
- `src/fragments/*.ts` → extends GraphQL query fragments
- `src/graphql/` → extends the GraphQL API
- `src/scripts/ThirdPartyScripts.tsx` → injected into `<head>`
4. Runs Next.js build/dev on the generated app
**You never create Next.js pages, `_app.tsx`, `_document.tsx`, or routing files.** FastStore Core owns those.
## Naming Conventions
| What | Convention | Example |
| ----------------------- | ---------------- | --------------------------------------- |
| Stylesheet filenames | kebab-case | `custom-button.module.scss` |
| Component files | PascalCase | `CustomButton.tsx` |
| Component exports | PascalCase | `export default CustomButton` |
| Function exports | camelCase | `export const getButtonVariants` |
| Constants | UPPER_SNAKE_CASE | `const BUTTON_VARIANTS` |
| Section folders | PascalCase | `src/components/sections/CustomButton/` |
| GraphQL files | camelCase | `contactForm.graphql` |
| Fragment files | PascalCase | `ServerProduct.ts`, `ClientProduct.ts` |
| Export key in index.tsx | PascalCase | `CustomButton` |
| $componentKey in JSONC | PascalCase | `CustomButton` |
⚠️ **CRITICAL**: The $componentKey in JSONC MUST match the export key in index.tsx EXACTLY.
## Experimental Imports
Imports from `@faststore/core/experimental` have an `_unstable` suffix and may change between versions:
```tsx
import { useNewsletter_unstable as useNewsletter } from "@faststore/core/experimental";
import { useLazyQuery_unstable as useLazyQuery } from "@faststore/core/experimental";
import { Image_unstable as Image } from "@faststore/core/experimental";
```
## PLP/Search Contexts
PLP pages list variable numbers of items. Two URL patterns exist:
- `{host}/s` — search pages
- `{host}/{...slug}` — category PLP pages
Facets (filters) are indexed by the Intelligent Search SDK. Configure at `https://{account}.myvtex.com/admin` → Catalog → Products and SKUs.
Facets are available via the `usePage<SearchPageContext | PLPContext>()` hook at `context?.data?.search?.facets`.
## Server vs Client Data Split
FastStore pages load data in two phases. **Not all data is available on both phases.**
| Page | Server query | Client query | Merged via |
| ------ | --------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------- |
| PLP | `ServerCollectionPageQuery` → collection, seo, breadcrumb | `ClientProductGalleryQuery` → products, **facets**, metadata | `deepmerge` in `ProductListing.tsx` → `PageProvider` |
| PDP | `ServerProductQuery` → product basics | `ClientProductQuery` → full product data | merged in `ProductDetailsPage.tsx` → `PageProvider` |
| Search | (none) | `ClientProductGalleryQuery` → products, **facets**, metadata | `ProductListing.tsx` → `PageProvider` |
**Custom sections using `usePage()` must handle both phases:**
- On first server render, only server data is available
- After client hydration, the merged data (including client fields like facets) becomes available
- Components should return `null` or a skeleton when expected client data is missing
references/scss-styling-and-design-tokens.md
---
name: faststore-styling
description: FastStore styling conventions including CSS custom properties (design tokens), SCSS modules for scoped component styles, component-level overrides using data-fs-* attributes, and how to import @faststore/ui styles for new sections. Use when styling components, creating SCSS modules, overriding design tokens in custom-theme.scss, targeting internal FastStore UI component styles, or importing styles for new custom sections.
metadata:
author: vtex
version: "1.0"
---
# FastStore Styling
## Core Rules
- All styling must use **SCSS** syntax in `.scss` files
- **No global SCSS** is permitted — all styles must be declared inside a wrapper class
- Import SCSS as **CSS Modules** inside components and apply it to the outmost wrapper element
- Prefer existing **CSS custom properties** (design tokens) over hardcoded values
- Create new variables only when no existing token fits
- In case of @faststore/core v3 is a dependency than use node-sass syntax if the it is a @faststore/core@v4 than most use dart sass.
## Design Tokens — `src/themes/custom-theme.scss`
FastStore uses CSS custom properties for theming. The token file is loaded based on the `theme` key in `discovery.config.js`.
```scss
// src/themes/custom-theme.scss
// Loaded automatically when discovery.config.js has `theme: "custom-theme"`
// Tokens must be declared inside @layer theme and .theme
@layer theme {
.theme {
// Colors
--fs-color-main-0: #fff;
// Component-level overrides using data-fs-* selectors
[data-fs-hero][data-fs-hero-color-variant="main"] {
--fs-hero-main-bkg-color: #000;
}
}
}
```
### Full tokens reference:
- [colors](https://developers.vtex.com/docs/guides/faststore/global-tokens-colors)
- [Typographi](https://developers.vtex.com/docs/guides/faststore/global-tokens-typography)
- [Spacing](https://developers.vtex.com/docs/guides/faststore/global-tokens-spacing)
- [Layout](https://developers.vtex.com/docs/guides/faststore/global-tokens-grid-and-layout)
- [Controls](https://developers.vtex.com/docs/guides/faststore/global-tokens-interactive-controls)
- [Refinements](https://developers.vtex.com/docs/guides/faststore/global-tokens-refinements)
## Component-Level Styles — SCSS Modules
Each component uses a `.module.scss` file for scoped styling:
```scss
// src/components/sections/CustomIconsAlert/custom-icons-alert.module.scss
.customIconsAlert {
// Target internal FastStore UI component structure with data-fs-* selectors
[data-fs-alert] {
justify-content: center;
color: var(--fs-color-neutral-0);
background-color: var(--fs-color-neutral-7);
}
[data-fs-icon],
[data-fs-link] {
color: var(--fs-color-neutral-0);
}
}
```
```tsx
// Import and apply in your component
import styles from "./custom-icons-alert.module.scss";
const CustomIconsAlert = getOverriddenSection({
Section: AlertSection,
className: styles.customIconsAlert, // Applied to the section root element
});
```
## Importing `@faststore/ui` Styles for New Sections
Native sections automatically get their styles. **New custom sections** must manually import styles for any `@faststore/ui` components they use.
### Hard constraint: `@import` of `@faststore/ui` styles must be nested inside a local class
CSS Modules require every selector in a `.module.scss` file to be "pure" — i.e., scoped under a local class or id. `@faststore/ui` component stylesheets declare selectors like `[data-fs-button]`, `[data-fs-input-field]`, etc. Importing them at the **root level** of a `.module.scss` file injects those global attribute selectors outside any local scope, triggering the build error:
> `Selector "[data-fs-button]" is not pure (pure selectors must contain at least one local class or id)`
**Why this matters:** The build fails. No workaround exists at the bundler level — the SCSS module spec enforces purity.
**Detection:** Look for any `@import` or `@use` of `@faststore/ui` component styles that is **not** nested inside a local class in a `.module.scss` file.
**Correct** — imports nested inside a local class:
```scss
// src/components/ContactForm/contact-form.module.scss
.contactForm {
@import "@faststore/ui/src/components/atoms/Button/styles.scss";
@import "@faststore/ui/src/components/atoms/InputField/styles.scss";
@import "@faststore/ui/src/components/molecules/Textarea/styles.scss";
display: flex;
flex-direction: column;
gap: var(--fs-spacing-3);
}
```
**Wrong** — imports at root level of the module file:
```scss
// src/components/ContactForm/contact-form.module.scss
// These are at root scope — WILL FAIL with "not pure" error
@import "@faststore/ui/src/components/atoms/Button/styles.scss";
@import "@faststore/ui/src/components/atoms/InputField/styles.scss";
.contactForm {
display: flex;
}
```
**Alternative — use native HTML when `@faststore/ui` is not needed:**
If the component only needs a styled `<button>` (no FastStore variants, icons, or loading states), use a plain `<button>` element with your own SCSS instead of importing `UIButton` styles. This avoids the import altogether and keeps the bundle smaller.
> **Note (dart-sass / v4):** If `@faststore/core` is v4, use `@use` instead of `@import`. The nesting-inside-a-local-class rule still applies — `@use` at the module root triggers the same purity error.
## Targeting `@faststore/ui` Component Internals
`@faststore/ui` components use `data-fs-*` attributes for their internal structure. Target these in your SCSS to style sub-elements without worrying about class name conflicts:
```scss
.mySection {
// Target the Button component internals
[data-fs-button] {
background-color: var(--fs-color-primary-bkg);
}
// Target a specific button variant
[data-fs-button][data-fs-button-variant="primary"] {
color: var(--fs-color-primary-text);
}
}
```
See [references/REFERENCE.md](references/REFERENCE.md) for the complete list of all `data-fs-*` styling attributes and design token custom properties.
### Hard constraint: Prefer native HTML elements over `@faststore/ui` components when the design is fully custom
`@faststore/ui` components (`UIButton`, `UIInput`, etc.) ship with internal styles bound to `[data-fs-*]` attribute selectors. When you import their stylesheets and then try to override most visual properties (background, padding, border-radius, font, etc.), the `[data-fs-button]` selectors from the imported stylesheet compete with your custom rules. Because both sit inside the same local class scope, the outcome depends on source order and specificity — leading to inconsistent results, `!important` escalation, or styles that break on upgrades.
**Why this matters:** Specificity conflicts produce visual bugs that are hard to trace. Developers resort to `!important` or increasingly complex selectors, making the stylesheet fragile and unmaintainable.
**Detection:** A custom section uses a `@faststore/ui` component **and** overrides more than 2–3 visual properties of that component in SCSS. If the custom design shares almost nothing with the default FastStore appearance, the component is adding overhead with no benefit.
**When to use `@faststore/ui` components:**
- The design is close to the default FastStore look (minor color/spacing tweaks)
- You need built-in behavior: button loading states, input validation, icon slots, accessibility attributes
- You are overriding a native section via `getOverriddenSection` (the slot already uses the UI component)
**When to use native HTML elements instead:**
- The design is completely custom (different shape, layout, animations)
- The component is purely presentational with no special FastStore behavior
- You find yourself overriding most `data-fs-*` default styles
**Correct** — native `<button>` when the design is fully custom:
```tsx
// src/components/sections/CustomCTA/CustomCTA.tsx
import styles from "./custom-cta.module.scss";
export default function CustomCTA() {
return (
<section className={styles.customCta}>
<button className={styles.ctaButton} type="button">
Shop Now
</button>
</section>
);
}
```
```scss
// src/components/sections/CustomCTA/custom-cta.module.scss
.customCta {
display: flex;
justify-content: center;
padding: var(--fs-spacing-6);
}
.ctaButton {
background: linear-gradient(135deg, #ff6b6b, #ee5a24);
border: none;
border-radius: 50px;
color: #fff;
padding: var(--fs-spacing-3) var(--fs-spacing-6);
font-size: var(--fs-text-size-body);
cursor: pointer;
}
```
**Wrong** — importing `UIButton` styles then fighting specificity:
```scss
// custom-cta.module.scss
.customCta {
@import "@faststore/ui/src/components/atoms/Button/styles.scss";
[data-fs-button] {
// Overriding almost everything — the import is wasted
background: linear-gradient(135deg, #ff6b6b, #ee5a24) !important;
border: none !important;
border-radius: 50px !important;
padding: var(--fs-spacing-3) var(--fs-spacing-6) !important;
}
}
```
## Using `@layer components` in Custom Sections
FastStore native sections wrap their styles in `@layer components`. This CSS cascade layer sits **below** `@layer theme` in specificity, so theme token overrides in `custom-theme.scss` always win without needing `!important`.
When building **new custom sections** (not overrides), use the same pattern to keep your section's styles consistent with the cascade order used by the rest of the storefront:
```scss
// src/components/sections/DailyOffers/daily-offers.module.scss
@layer components {
.dailyOffers {
@import "@faststore/ui/src/components/atoms/Button/styles.scss";
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: var(--fs-spacing-4);
padding: var(--fs-spacing-6) var(--fs-grid-padding);
[data-fs-button] {
min-width: 180px;
}
}
}
```
**Why use `@layer components`:**
- Theme tokens declared in `@layer theme { .theme { ... } }` automatically override component-layer styles — no `!important` needed
- Consistent cascade behavior with native sections: your custom section responds to theme changes the same way built-in sections do
- Future-proof: if FastStore adds new cascade layers, sections in `@layer components` stay in the right place
**When to skip `@layer components`:**
- Section overrides via `getOverriddenSection` — the native section already provides the layer; your `.module.scss` only adds scoped tweaks on top
- Sections that use only native HTML elements with fully custom styles and no `@faststore/ui` components — there are no `data-fs-*` styles to layer against
## Common Design Tokens
| Token category | Example tokens |
|---------------|---------------|
| Colors | `--fs-color-main-0`, `--fs-color-neutral-0`, `--fs-color-primary-bkg` |
| Spacing | `--fs-spacing-0` through `--fs-spacing-10` |
| Typography | `--fs-text-face-body`, `--fs-text-size-base` |
| Border | `--fs-border-radius-default`, `--fs-border-width-default` |
| Shadow | `--fs-shadow-0` through `--fs-shadow-3` |
# FastStore Styling
All Faststore CSS styling conventions and custom properties (design tokens and component variables) defined across the FastStore UI package, organized by design category.
## Design tokens (CSS custom properties)
FastStore uses **CSS custom properties** (design tokens) for theming. The token file is referenced by the `theme` key in `discovery.config.js`.
```scss
// src/themes/custom-theme.scss
// Global design token overrides. This file is automatically loaded by FastStore CLI
// when discovery.config.js has `theme: "custom-theme"`.
//
// Tokens are defined inside `@layer theme` and `.theme` to ensure correct specificity
// in the CSS cascade. FastStore Core applies the `.theme` class to the root element.
//
// Full token reference: https://developers.vtex.com/docs/guides/faststore/global-tokens-overview
@layer theme {
.theme {
// --------------------------------------------------------
// Colors (Branding Core)
// Override --fs-color-* tokens to change the global palette.
// --------------------------------------------------------
--fs-color-main-0: #fff;
// --------------------------------------------------------
// Typography (Branding Core)
// Override --fs-text-face-*, --fs-text-size-* tokens here.
// --------------------------------------------------------
// --------------------------------------------------------
// Spacing (UI Essentials)
// Override --fs-spacing-* tokens here.
// --------------------------------------------------------
// --------------------------------------------------------
// FS UI Component-level overrides
// Target specific components using their data-fs-* attributes.
// --------------------------------------------------------
[data-fs-hero][data-fs-hero-color-variant="main"] {
--fs-hero-main-bkg-color: #000;
}
}
}
```
## Component-level styles (SCSS modules)
Each component uses **CSS Modules** (`.module.scss`) for scoped styling:
```scss
// src/components/sections/CustomIconsAlert/custom-icons-alert.module.scss
// Scoped styles for the CustomIconsAlert section.
// Uses FastStore design tokens (--fs-color-*) for consistency.
// data-fs-* selectors target the internal structure of FastStore UI components.
.customIconsAlert {
[data-fs-alert] {
justify-content: center;
color: var(--fs-color-neutral-0);
background-color: var(--fs-color-neutral-7);
}
[data-fs-icon],
[data-fs-link] {
color: var(--fs-color-neutral-0);
}
}
```
## Importing `@faststore/ui` styles for new sections
Native sections get their styles automatically. **New sections** (not overrides) must manually import the styles for any `@faststore/ui` components they use.
> **All `@import` / `@use` of `@faststore/ui` styles must be inside a local class** — never at the module root. Root-level imports inject `[data-fs-*]` attribute selectors that break CSS Modules purity. See the hard constraint above for details.
```scss
// src/components/ContactForm/contact-form.module.scss
// New sections don't inherit @faststore/ui styles automatically.
// Import them explicitly for each UI component used.
// Docs: https://developers.vtex.com/docs/guides/faststore/using-themes-importing-ui-components-styles
.contactForm {
// Utilities import must also be inside the local class
@import "@faststore/ui/src/styles/base/utilities";
@import "~@faststore/ui/src/components/atoms/Button/styles.scss";
@import "~@faststore/ui/src/components/atoms/Input/styles.scss";
@import "~@faststore/ui/src/components/molecules/InputField/styles.scss";
// Media mixin breakpoints (from utilities):
// "phone": 320px, "phonemid": 375px, "tablet": 768px,
// "notebook": 1280px, "desktop": 1440px
@include media(">=notebook") {
align-items: center;
justify-content: space-between;
}
@include media("<=phone") {
align-items: left;
}
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: var(--fs-spacing-2);
> div,
form {
margin: var(--fs-spacing-6) 0 var(--fs-spacing-3);
}
h2 {
font-family: var(--fs-text-face-title);
font-size: var(--fs-text-size-title-page);
margin: var(--fs-spacing-6) 0 var(--fs-spacing-3);
}
[data-fs-input-field] {
margin: var(--fs-spacing-3) 0;
}
[data-fs-textarea] {
width: 100%;
margin-bottom: var(--fs-spacing-4);
padding: var(--fs-spacing-2) var(--fs-spacing-2) 0;
height: 150px;
}
[data-fs-button] {
min-width: 250px;
margin: auto;
}
}
```
## Styling conventions
- Use `data-fs-*` attribute selectors to target FastStore UI component internals.
- Use `@layer theme` for global token overrides.
- Use `@layer components` for new custom section styles so they participate in the same cascade as native sections.
- Use `.module.scss` for component-scoped styles.
- Import `@faststore/ui` component styles explicitly in new (non-override) sections — **always inside a local class, never at the module root**.
- Use `@faststore/ui/src/styles/base/utilities` mixin `@include media` for media query breakpoints styles.
- When a `@faststore/ui` component is only used for basic HTML semantics (e.g., a plain `<button>`), prefer the native element with custom SCSS instead of importing `@faststore/ui` styles.
- **Do not import `@faststore/ui` component styles and then override most of their visual properties** — use native HTML elements instead to avoid specificity conflicts and `!important` escalation.
## Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
| `Selector "[data-fs-*]" is not pure (pure selectors must contain at least one local class or id)` | `@import` / `@use` of `@faststore/ui` component styles at root level of a `.module.scss` file | Move the import inside the wrapper local class (e.g., `.section { @import "..."; }`) |
| Unstyled `@faststore/ui` components in a new custom section | Missing explicit style import for the UI component | Add `@import "@faststore/ui/src/components/.../<Component>/styles.scss"` inside the wrapper class |
| `@include media(...)` not found | `@faststore/ui/src/styles/base/utilities` not imported | Import utilities inside the wrapper class before using the mixin |
| Custom styles on `UIButton` / `UIInput` don't apply or require `!important` | Specificity conflict with imported `@faststore/ui` `[data-fs-*]` styles | If the design is fully custom, replace the `@faststore/ui` component with a native HTML element and remove the style import |
| Theme token overrides in `custom-theme.scss` don't affect a custom section | Custom section styles are not inside `@layer components` | Wrap the section's `.module.scss` styles in `@layer components { ... }` so `@layer theme` wins |
---
# Tokens reference
## Global Design Tokens
Defined in `packages/ui/src/styles/base/tokens.scss`.
---
### Colors — Palette
Raw brand palette. Tone values range from lightest (`0`) to darkest (`4`).
| Variable | Default Value |
|---|---|
| `--fs-color-main-0` | `#f1f2f3` |
| `--fs-color-main-1` | `#dbdbdb` |
| `--fs-color-main-2` | `#00419e` |
| `--fs-color-main-3` | `#002c71` |
| `--fs-color-main-4` | `#002155` |
| `--fs-color-accent-0` | `#efeaf5` |
| `--fs-color-accent-1` | `#d3c9de` |
| `--fs-color-accent-2` | `#9d8abf` |
| `--fs-color-accent-3` | `#74678c` |
| `--fs-color-accent-4` | `#423759` |
| `--fs-color-neutral-0` | `#ffffff` |
| `--fs-color-neutral-1` | `#f1f2f3` |
| `--fs-color-neutral-2` | `#e3e6e8` |
| `--fs-color-neutral-3` | `#c7ccd1` |
| `--fs-color-neutral-4` | `#9099a2` |
| `--fs-color-neutral-5` | `#74808b` |
| `--fs-color-neutral-6` | `#5d666f` |
| `--fs-color-neutral-7` | `#171a1c` |
---
### Colors — Hierarchy
Semantic color aliases for UI element hierarchy (primary, secondary, tertiary, action).
| Variable | Default Value |
|---|---|
| `--fs-color-primary-text` | `var(--fs-color-text-inverse)` |
| `--fs-color-primary-bkg` | `var(--fs-color-main-2)` |
| `--fs-color-primary-bkg-hover` | `var(--fs-color-main-3)` |
| `--fs-color-primary-bkg-active` | `var(--fs-color-main-4)` |
| `--fs-color-primary-bkg-light` | `var(--fs-color-main-0)` |
| `--fs-color-primary-bkg-light-active` | `var(--fs-color-main-1)` |
| `--fs-color-secondary-text` | `var(--fs-color-primary-bkg)` |
| `--fs-color-secondary-bkg` | `transparent` |
| `--fs-color-secondary-bkg-hover` | `var(--fs-color-primary-bkg)` |
| `--fs-color-secondary-bkg-active` | `var(--fs-color-main-3)` |
| `--fs-color-secondary-bkg-light` | `var(--fs-color-main-0)` |
| `--fs-color-secondary-bkg-light-active` | `var(--fs-color-secondary-bkg-light)` |
| `--fs-color-tertiary-text` | `var(--fs-color-link)` |
| `--fs-color-tertiary-bkg` | `transparent` |
| `--fs-color-tertiary-bkg-hover` | `var(--fs-color-main-0)` |
| `--fs-color-tertiary-bkg-active` | `var(--fs-color-main-1)` |
| `--fs-color-tertiary-bkg-light` | `var(--fs-color-neutral-0)` |
| `--fs-color-tertiary-bkg-light-active` | `var(--fs-color-tertiary-bkg-light)` |
| `--fs-color-action-text` | `var(--fs-color-text-inverse)` |
| `--fs-color-action-bkg` | `var(--fs-color-accent-4)` |
| `--fs-color-action-bkg-hover` | `var(--fs-color-accent-3)` |
| `--fs-color-action-bkg-active` | `var(--fs-color-accent-2)` |
| `--fs-color-action-bkg-light` | `var(--fs-color-neutral-0)` |
| `--fs-color-action-bkg-light-active` | `var(--fs-color-tertiary-bkg-light)` |
---
### Colors — Body, Text & Link
| Variable | Default Value |
|---|---|
| `--fs-color-body-bkg` | `var(--fs-color-neutral-0)` |
| `--fs-body-bkg` | `var(--fs-color-body-bkg)` |
| `--fs-color-text` | `var(--fs-color-neutral-7)` |
| `--fs-color-text-light` | `var(--fs-color-neutral-6)` |
| `--fs-color-text-inverse` | `var(--fs-color-neutral-0)` |
| `--fs-color-text-display` | `var(--fs-color-neutral-7)` |
| `--fs-color-link` | `var(--fs-color-main-2)` |
| `--fs-color-link-hover` | `var(--fs-color-main-2)` |
| `--fs-color-link-active` | `var(--fs-color-main-4)` |
| `--fs-color-link-visited` | `#6058ba` |
| `--fs-color-link-inverse` | `var(--fs-color-neutral-0)` |
| `--fs-color-disabled-text` | `var(--fs-color-neutral-6)` |
| `--fs-color-disabled-bkg` | `var(--fs-color-neutral-2)` |
---
### Colors — Focus Ring
| Variable | Default Value |
|---|---|
| `--fs-color-focus-ring` | `#8db6fa` |
| `--fs-color-focus-ring-outline` | `#8db6fa80` |
| `--fs-color-focus-ring-danger` | `#e1adad` |
---
### Colors — Situations
Semantic feedback colors for success, warning, danger, info, highlighted, and neutral states.
| Variable | Default Value |
|---|---|
| `--fs-color-success-0` | `#1e493b` |
| `--fs-color-success-1` | `#b3ebd5` |
| `--fs-color-success-2` | `#016810` |
| `--fs-color-success-text` | `var(--fs-color-success-0)` |
| `--fs-color-success-bkg` | `var(--fs-color-success-1)` |
| `--fs-color-success-border` | `var(--fs-color-success-text)` |
| `--fs-color-warning-text` | `var(--fs-color-text)` |
| `--fs-color-warning-bkg` | `#fdec8d` |
| `--fs-color-warning-border` | `var(--fs-color-warning-text)` |
| `--fs-color-danger-text` | `#cb4242` |
| `--fs-color-danger-bkg` | `var(--fs-color-focus-ring-danger)` |
| `--fs-color-danger-border` | `var(--fs-color-danger-text)` |
| `--fs-color-info-text` | `var(--fs-color-text)` |
| `--fs-color-info-bkg` | `var(--fs-color-main-1)` |
| `--fs-color-highlighted-text` | `var(--fs-color-text-display)` |
| `--fs-color-highlighted-bkg` | `var(--fs-color-accent-0)` |
| `--fs-color-neutral-text` | `var(--fs-color-text)` |
| `--fs-color-neutral-bkg` | `var(--fs-color-neutral-1)` |
---
### Typography — Font Face
| Variable | Default Value |
|---|---|
| `--fs-text-face-body` | `-apple-system, system-ui, BlinkMacSystemFont, sans-serif` |
| `--fs-text-face-title` | `var(--fs-text-face-body)` |
---
### Typography — Font Weight
| Variable | Default Value |
|---|---|
| `--fs-text-weight-light` | `300` |
| `--fs-text-weight-regular` | `400` |
| `--fs-text-weight-medium` | `500` |
| `--fs-text-weight-semibold` | `600` |
| `--fs-text-weight-bold` | `700` |
| `--fs-text-weight-black` | `900` |
---
### Typography — Numeric Scale
Fluid scale with breakpoint overrides: mobile step = `2px`, desktop step = `4px`.
| Variable | Mobile | Desktop |
|---|---|---|
| `--fs-text-size-base` | `16px` | `16px` |
| `--fs-text-size-0` | `12px` | `12px` |
| `--fs-text-size-1` | `14px` | `14px` |
| `--fs-text-size-2` | `16px` | `16px` |
| `--fs-text-size-3` | `18px` | `20px` |
| `--fs-text-size-4` | `20px` | `24px` |
| `--fs-text-size-5` | `22px` | `28px` |
| `--fs-text-size-6` | `24px` | `32px` |
| `--fs-text-size-7` | `28px` | `40px` |
| `--fs-text-size-8` | `32px` | `48px` |
| `--fs-text-size-9` | `36px` | `56px` |
| `--fs-text-scale-mobile` | `2px` | — |
| `--fs-text-scale-desktop` | — | `4px` |
| `--fs-scale` | `var(--fs-text-scale-mobile)` | `var(--fs-text-scale-desktop)` |
| `--fs-text-max-lines` | `2` | — |
---
### Typography — Semantic Sizes
| Variable | Default Value |
|---|---|
| `--fs-text-size-title-huge` | `var(--fs-text-size-8)` |
| `--fs-text-size-title-page` | `var(--fs-text-size-7)` |
| `--fs-text-size-title-product` | `var(--fs-text-size-4)` |
| `--fs-text-size-title-section` | `var(--fs-text-size-4)` |
| `--fs-text-size-title-subsection` | `var(--fs-text-size-4)` |
| `--fs-text-size-title-mini` | `var(--fs-text-size-4)` |
| `--fs-text-size-lead` | `var(--fs-text-size-3)` |
| `--fs-text-size-menu` | `var(--fs-text-size-base)` |
| `--fs-text-size-body` | `var(--fs-text-size-base)` |
| `--fs-text-size-legend` | `var(--fs-text-size-1)` |
| `--fs-text-size-tiny` | `var(--fs-text-size-0)` |
---
### Spacing
8-point-like scale from `4px` to `96px`.
| Variable | Value |
|---|---|
| `--fs-spacing-0` | `.25rem` (4px) |
| `--fs-spacing-1` | `.5rem` (8px) |
| `--fs-spacing-2` | `.75rem` (12px) |
| `--fs-spacing-3` | `1rem` (16px) |
| `--fs-spacing-4` | `1.5rem` (24px) |
| `--fs-spacing-5` | `2rem` (32px) |
| `--fs-spacing-6` | `2.5rem` (40px) |
| `--fs-spacing-7` | `3rem` (48px) |
| `--fs-spacing-8` | `3.5rem` (56px) |
| `--fs-spacing-9` | `4rem` (64px) |
| `--fs-spacing-10` | `4.5rem` (72px) |
| `--fs-spacing-11` | `5rem` (80px) |
| `--fs-spacing-12` | `5.5rem` (88px) |
| `--fs-spacing-13` | `6rem` (96px) |
---
### Grid & Layout
| Variable | Default Value |
|---|---|
| **Padding** | |
| `--fs-grid-padding` | `var(--fs-spacing-3)` (≥tablet: `--fs-spacing-4`, ≥notebook: `--fs-spacing-5`) |
| **Container** | |
| `--fs-grid-max-width` | `calc(--fs-grid-breakpoint-notebook - 2 × --fs-grid-padding)` |
| **Gaps** | |
| `--fs-grid-gap-0` | `var(--fs-spacing-1)` |
| `--fs-grid-gap-1` | `var(--fs-spacing-2)` |
| `--fs-grid-gap-2` | `var(--fs-spacing-3)` |
| `--fs-grid-gap-3` | `var(--fs-spacing-4)` |
| `--fs-grid-gap-4` | `var(--fs-spacing-5)` |
| **Breakpoints** | |
| `--fs-grid-breakpoint-phone` | Sass map value |
| `--fs-grid-breakpoint-phonemid` | Sass map value |
| `--fs-grid-breakpoint-tablet` | Sass map value |
| `--fs-grid-breakpoint-notebook` | Sass map value |
| `--fs-grid-breakpoint-desktop` | Sass map value |
| **Z-Index** | |
| `--fs-z-index-below` | `-1` |
| `--fs-z-index-default` | `0` |
| `--fs-z-index-top` | `1` |
| `--fs-z-index-high` | `2` |
| `--fs-z-index-highest` | `3` |
---
### Interactive Controls
| Variable | Default Value |
|---|---|
| `--fs-control-tap-size` | `var(--fs-spacing-7)` (48px) |
| `--fs-control-tap-size-smallest` | `calc(--fs-control-tap-size / 2)` |
| `--fs-control-min-height` | `var(--fs-control-tap-size)` |
| `--fs-control-bkg` | `var(--fs-color-neutral-0)` |
| `--fs-control-bkg-disabled` | `var(--fs-color-disabled-bkg)` |
---
### Transitions & Animation
| Variable | Default Value |
|---|---|
| `--fs-transition-timing` | `.2s` |
| `--fs-transition-property` | `all` |
| `--fs-transition-function` | `ease-in-out` |
---
### Borders — Radius
| Variable | Default Value |
|---|---|
| `--fs-border-radius-small` | `1px` |
| `--fs-border-radius` | `2px` |
| `--fs-border-radius-medium` | `8px` |
| `--fs-border-radius-pill` | `100px` |
| `--fs-border-radius-circle` | `100%` |
---
### Borders — Width
| Variable | Default Value |
|---|---|
| `--fs-border-width` | `1px` |
| `--fs-border-width-thick` | `2px` |
| `--fs-border-width-thickest` | `3px` |
---
### Borders — Color
| Variable | Default Value |
|---|---|
| `--fs-border-color` | `var(--fs-color-neutral-4)` |
| `--fs-border-color-hover` | `var(--fs-color-main-3)` |
| `--fs-border-color-active` | `var(--fs-color-main-2)` |
| `--fs-border-color-disabled` | `var(--fs-color-neutral-6)` |
| `--fs-border-color-light` | `var(--fs-color-neutral-2)` |
| `--fs-border-color-light-hover` | `var(--fs-color-neutral-3)` |
| `--fs-border-color-light-active` | `var(--fs-color-neutral-3)` |
| `--fs-border-color-light-disabled` | `var(--fs-color-neutral-5)` |
---
### Shadows
| Variable | Default Value |
|---|---|
| `--fs-shadow` | `none` |
| `--fs-shadow-darker` | `0 0 10px rgb(0 0 0 / 20%)` |
| `--fs-shadow-hover` | `0 2px 3px rgb(0 0 0 / 10%)` |
---
### Miscellaneous
| Variable | Default Value |
|---|---|
| `--fs-logo-width` | `7rem` |
---
## Component Variables
---
## Atoms
### Badge
| Variable | Default Value |
|---|---|
| `--fs-badge-padding` | `var(--fs-spacing-0) var(--fs-spacing-2)` |
| `--fs-badge-big-padding` | `var(--fs-spacing-1) var(--fs-spacing-2)` |
| `--fs-badge-bkg-color` | `var(--fs-color-neutral-bkg)` |
| `--fs-badge-border-color` | `transparent` |
| `--fs-badge-border-radius` | `var(--fs-border-radius-pill)` |
| `--fs-badge-border-style` | `none` |
| `--fs-badge-border-width` | `0` |
| `--fs-badge-text-color` | `var(--fs-color-text)` |
| `--fs-badge-text-size` | `var(--fs-text-size-tiny)` |
| `--fs-badge-big-text-size` | `var(--fs-text-size-legend)` |
| `--fs-badge-text-weight` | `var(--fs-text-weight-bold)` |
| `--fs-badge-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-badge-transition-property` | `var(--fs-transition-property)` |
| `--fs-badge-transition-function` | `var(--fs-transition-function)` |
| **Variants** | |
| `--fs-badge-success-bkg-color` | `var(--fs-color-success-bkg)` |
| `--fs-badge-success-border-color` | `var(--fs-color-success-bkg)` |
| `--fs-badge-success-text-color` | `var(--fs-badge-text-color)` |
| `--fs-badge-warning-bkg-color` | `var(--fs-color-warning-bkg)` |
| `--fs-badge-warning-border-color` | `var(--fs-color-warning-bkg)` |
| `--fs-badge-warning-text-color` | `var(--fs-color-warning-text)` |
| `--fs-badge-danger-bkg-color` | `var(--fs-color-danger-bkg)` |
| `--fs-badge-danger-border-color` | `var(--fs-color-danger-bkg)` |
| `--fs-badge-danger-text-color` | `var(--fs-badge-text-color)` |
| `--fs-badge-info-bkg-color` | `var(--fs-color-info-bkg)` |
| `--fs-badge-info-border-color` | `var(--fs-color-info-bkg)` |
| `--fs-badge-info-text-color` | `var(--fs-color-info-text)` |
| `--fs-badge-highlighted-bkg-color` | `var(--fs-color-highlighted-bkg)` |
| `--fs-badge-highlighted-border-color` | `var(--fs-color-highlighted-bkg)` |
| `--fs-badge-highlighted-text-color` | `var(--fs-color-highlighted-text)` |
| `--fs-badge-neutral-bkg-color` | `var(--fs-color-neutral-bkg)` |
| `--fs-badge-neutral-border-color` | `var(--fs-color-neutral-bkg)` |
| `--fs-badge-neutral-text-color` | `var(--fs-badge-text-color)` |
| **Counter** | |
| `--fs-badge-counter-bkg-color` | `var(--fs-color-link)` |
| `--fs-badge-counter-border-color` | `var(--fs-color-body-bkg)` |
| `--fs-badge-counter-border-radius` | `var(--fs-border-radius-pill)` |
| `--fs-badge-counter-padding` | `var(--fs-spacing-0)` |
| `--fs-badge-counter-size` | `var(--fs-spacing-3)` |
| `--fs-badge-counter-text-color` | `var(--fs-color-text-inverse)` |
| `--fs-badge-counter-text-size` | `var(--fs-text-size-0)` |
---
### Button
| Variable | Default Value |
|---|---|
| **Size & Shape** | |
| `--fs-button-height` | `var(--fs-control-tap-size)` |
| `--fs-button-padding` | `calc(var(--fs-spacing-1) - (var(--fs-button-border-width) * 2)) var(--fs-spacing-3)` |
| `--fs-button-gap` | `var(--fs-spacing-2)` |
| `--fs-button-icon-padding` | `0 var(--fs-spacing-1)` |
| `--fs-button-border-radius` | `var(--fs-border-radius)` |
| `--fs-button-border-width` | `var(--fs-border-width-thick)` |
| `--fs-button-border-color` | `transparent` |
| **Typography** | |
| `--fs-button-text-size` | `var(--fs-text-size-base)` |
| `--fs-button-text-weight` | `var(--fs-text-weight-bold)` |
| **States — Disabled** | |
| `--fs-button-disabled-bkg-color` | `var(--fs-color-disabled-bkg)` |
| `--fs-button-disabled-text-color` | `var(--fs-color-disabled-text)` |
| **Shadow** | |
| `--fs-button-shadow` | `var(--fs-shadow)` |
| `--fs-button-shadow-hover` | `var(--fs-button-shadow)` |
| **Loading** | |
| `--fs-button-loading-label-column-gap` | `var(--fs-spacing-3)` |
| **Transition** | |
| `--fs-button-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-button-transition-property` | `var(--fs-transition-property)` |
| `--fs-button-transition-function` | `var(--fs-transition-function)` |
| **Small variant** | |
| `--fs-button-small-gap` | `var(--fs-spacing-1)` |
| `--fs-button-small-padding` | `var(--fs-spacing-0) var(--fs-spacing-1)` |
| `--fs-button-small-min-height` | `var(--fs-spacing-7)` |
| `--fs-button-small-icon-width` | `var(--fs-spacing-3)` |
| `--fs-button-small-icon-height` | `var(--fs-button-small-icon-width)` |
| **Primary** | |
| `--fs-button-primary-bkg-color` | `var(--fs-color-primary-bkg)` |
| `--fs-button-primary-bkg-color-hover` | `var(--fs-color-primary-bkg-hover)` |
| `--fs-button-primary-bkg-color-active` | `var(--fs-color-primary-bkg-active)` |
| `--fs-button-primary-text-color` | `var(--fs-color-primary-text)` |
| `--fs-button-primary-text-color-hover` | `var(--fs-button-primary-text-color)` |
| `--fs-button-primary-text-color-active` | `var(--fs-button-primary-text-color)` |
| `--fs-button-primary-border-color` | `transparent` |
| `--fs-button-primary-border-color-hover` | `var(--fs-button-primary-border-color)` |
| `--fs-button-primary-border-color-active` | `var(--fs-button-primary-border-color)` |
| `--fs-button-primary-shadow-hover` | `var(--fs-button-shadow-hover)` |
| **Primary Inverse** | |
| `--fs-button-primary-inverse-bkg-color` | `var(--fs-button-primary-text-color)` |
| `--fs-button-primary-inverse-bkg-color-hover` | `var(--fs-color-primary-bkg-light)` |
| `--fs-button-primary-inverse-bkg-color-active` | `var(--fs-color-primary-bkg-light-active)` |
| `--fs-button-primary-inverse-text-color` | `var(--fs-button-primary-bkg-color)` |
| `--fs-button-primary-inverse-text-color-hover` | `var(--fs-button-primary-bkg-color)` |
| `--fs-button-primary-inverse-text-color-active` | `var(--fs-button-primary-bkg-color)` |
| `--fs-button-primary-inverse-border-color` | `var(--fs-button-primary-border-color)` |
| `--fs-button-primary-inverse-border-color-hover` | `var(--fs-button-primary-border-color)` |
| `--fs-button-primary-inverse-border-color-active` | `var(--fs-button-primary-border-color)` |
| `--fs-button-primary-inverse-shadow-hover` | `var(--fs-button-shadow-hover)` |
| **Secondary** | |
| `--fs-button-secondary-bkg-color` | `var(--fs-color-secondary-bkg)` |
| `--fs-button-secondary-bkg-color-hover` | `var(--fs-color-secondary-bkg-hover)` |
| `--fs-button-secondary-bkg-color-active` | `var(--fs-color-secondary-bkg-active)` |
| `--fs-button-secondary-text-color` | `var(--fs-color-secondary-text)` |
| `--fs-button-secondary-text-color-hover` | `var(--fs-color-text-inverse)` |
| `--fs-button-secondary-text-color-active` | `var(--fs-button-secondary-text-color-hover)` |
| `--fs-button-secondary-border-color` | `var(--fs-button-secondary-text-color)` |
| `--fs-button-secondary-border-color-hover` | `var(--fs-button-secondary-bkg-color-hover)` |
| `--fs-button-secondary-border-color-active` | `var(--fs-button-secondary-bkg-color-active)` |
| `--fs-button-secondary-shadow-hover` | `var(--fs-button-shadow-hover)` |
| **Secondary Inverse** | |
| `--fs-button-secondary-inverse-bkg-color` | `var(--fs-button-secondary-bkg-color)` |
| `--fs-button-secondary-inverse-bkg-color-hover` | `var(--fs-button-secondary-text-color-hover)` |
| `--fs-button-secondary-inverse-bkg-color-active` | `var(--fs-color-secondary-bkg-light)` |
| `--fs-button-secondary-inverse-text-color` | `var(--fs-button-secondary-text-color-hover)` |
| `--fs-button-secondary-inverse-text-color-hover` | `var(--fs-button-secondary-text-color)` |
| `--fs-button-secondary-inverse-text-color-active` | `var(--fs-button-secondary-inverse-text-color-hover)` |
| `--fs-button-secondary-inverse-border-color` | `var(--fs-button-secondary-inverse-text-color)` |
| `--fs-button-secondary-inverse-border-color-hover` | `var(--fs-button-secondary-inverse-bkg-color-hover)` |
| `--fs-button-secondary-inverse-border-color-active` | `var(--fs-button-secondary-inverse-bkg-color-active)` |
| `--fs-button-secondary-inverse-shadow-hover` | `var(--fs-button-shadow-hover)` |
| **Tertiary** | |
| `--fs-button-tertiary-bkg-color` | `var(--fs-color-tertiary-bkg)` |
| `--fs-button-tertiary-bkg-color-hover` | `var(--fs-color-tertiary-bkg-hover)` |
| `--fs-button-tertiary-bkg-color-active` | `var(--fs-color-tertiary-bkg-active)` |
| `--fs-button-tertiary-text-color` | `var(--fs-color-tertiary-text)` |
| `--fs-button-tertiary-text-color-hover` | `var(--fs-button-tertiary-text-color)` |
| `--fs-button-tertiary-text-color-active` | `var(--fs-button-primary-bkg-color)` |
| `--fs-button-tertiary-border-color` | `transparent` |
| `--fs-button-tertiary-border-color-hover` | `var(--fs-button-tertiary-border-color)` |
| `--fs-button-tertiary-border-color-active` | `var(--fs-button-tertiary-border-color)` |
| `--fs-button-tertiary-shadow-hover` | `var(--fs-button-shadow-hover)` |
| **Tertiary Inverse** | |
| `--fs-button-tertiary-inverse-bkg-color` | `var(--fs-button-secondary-inverse-bkg-color)` |
| `--fs-button-tertiary-inverse-bkg-color-hover` | `var(--fs-button-primary-bkg-color-hover)` |
| `--fs-button-tertiary-inverse-bkg-color-active` | `var(--fs-button-primary-bkg-color-active)` |
| `--fs-button-tertiary-inverse-text-color` | `var(--fs-button-secondary-text-color-hover)` |
| `--fs-button-tertiary-inverse-text-color-hover` | `var(--fs-button-secondary-text-color-hover)` |
| `--fs-button-tertiary-inverse-text-color-active` | `var(--fs-button-secondary-text-color-hover)` |
| `--fs-button-tertiary-inverse-border-color` | `var(--fs-button-tertiary-border-color)` |
| `--fs-button-tertiary-inverse-border-color-hover` | `var(--fs-button-tertiary-border-color)` |
| `--fs-button-tertiary-inverse-border-color-active` | `var(--fs-button-tertiary-border-color)` |
| `--fs-button-tertiary-inverse-shadow-hover` | `var(--fs-button-shadow-hover)` |
---
### Checkbox
| Variable | Default Value |
|---|---|
| `--fs-checkbox-width` | `1.25rem` |
| `--fs-checkbox-height` | `var(--fs-checkbox-width)` |
| `--fs-checkbox-border-radius` | `var(--fs-border-radius)` |
| `--fs-checkbox-border-color` | `var(--fs-border-color)` |
| `--fs-checkbox-border-color-hover` | `var(--fs-border-color-active)` |
| `--fs-checkbox-border-width` | `var(--fs-border-width)` |
| `--fs-checkbox-bkg-color-hover` | `var(--fs-color-primary-bkg-light)` |
| `--fs-checkbox-shadow-hover` | `0 0 0 var(--fs-checkbox-border-width) var(--fs-border-color-active)` |
| `--fs-checkbox-transition` | `border, background-color, box-shadow` |
| **Checked** | |
| `--fs-checkbox-checked-bkg-color` | `var(--fs-color-primary-bkg)` |
| `--fs-checkbox-checked-bkg-color-hover` | `var(--fs-color-primary-bkg-hover)` |
| `--fs-checkbox-checked-border-color-hover` | `var(--fs-border-color-hover)` |
| `--fs-checkbox-checked-shadow-hover` | `0 0 0 var(--fs-checkbox-border-width) var(--fs-checkbox-checked-border-color-hover)` |
| **Partial** | |
| `--fs-checkbox-partial-bkg-color` | `var(--fs-color-body-bkg)` |
| `--fs-checkbox-partial-bkg-color-hover` | `var(--fs-color-primary-bkg-light)` |
| `--fs-checkbox-partial-border-color` | `var(--fs-color-primary-bkg)` |
| `--fs-checkbox-partial-border-width` | `var(--fs-checkbox-border-width)` |
| **Disabled** | |
| `--fs-checkbox-disabled-bkg-color` | `var(--fs-color-disabled-bkg)` |
| `--fs-checkbox-disabled-border-color` | `var(--fs-border-color-disabled)` |
| `--fs-checkbox-disabled-border-width` | `var(--fs-checkbox-border-width)` |
| `--fs-checkbox-disabled-text-color` | `var(--fs-color-disabled-text)` |
| **Field (label + error)** | |
| `--fs-checkbox-field-gap` | `var(--fs-spacing-1)` |
| `--fs-checkbox-field-label-color` | `var(--fs-color-text-light)` |
| `--fs-checkbox-field-label-line-height` | `1.42` |
| `--fs-checkbox-field-label-size` | `var(--fs-text-size-1)` |
| `--fs-checkbox-field-label-weight` | `var(--fs-text-weight-regular)` |
| `--fs-checkbox-field-error-border-color` | `var(--fs-color-danger-border)` |
| `--fs-checkbox-field-error-message-color` | `var(--fs-color-danger-text)` |
| `--fs-checkbox-field-error-message-line-height` | `1.1` |
| `--fs-checkbox-field-error-message-margin-top` | `var(--fs-spacing-0)` |
| `--fs-checkbox-field-error-message-size` | `var(--fs-text-size-legend)` |
---
### Input
| Variable | Default Value |
|---|---|
| `--fs-input-height` | `var(--fs-control-tap-size)` |
| `--fs-input-padding` | `var(--fs-spacing-1) var(--fs-spacing-2)` |
| `--fs-input-line-height` | `1.25` |
| `--fs-input-bkg-color` | `var(--fs-color-body-bkg)` |
| `--fs-input-border-color` | `var(--fs-border-color)` |
| `--fs-input-border-color-hover` | `var(--fs-border-color-active)` |
| `--fs-input-border-radius` | `var(--fs-border-radius)` |
| `--fs-input-border-width` | `var(--fs-border-width)` |
| `--fs-input-box-shadow` | `none` |
| `--fs-input-box-shadow-hover` | `0 0 0 var(--fs-border-width) var(--fs-border-color-active)` |
| `--fs-input-text-color` | `var(--fs-color-text)` |
| `--fs-input-text-size` | `var(--fs-text-size-body)` |
| `--fs-input-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-input-transition-property` | `var(--fs-transition-property)` |
| `--fs-input-transition-function` | `var(--fs-transition-function)` |
| **Disabled** | |
| `--fs-input-disabled-bkg-color` | `var(--fs-color-disabled-bkg)` |
| `--fs-input-disabled-border-color` | `var(--fs-border-color)` |
| `--fs-input-disabled-border-width` | `var(--fs-border-width)` |
| `--fs-input-disabled-text-color` | `var(--fs-color-disabled-text)` |
---
### Link
| Variable | Default Value |
|---|---|
| `--fs-link-padding` | `var(--fs-spacing-2) var(--fs-spacing-0)` |
| `--fs-link-small-padding` | `var(--fs-spacing-1) var(--fs-spacing-0)` |
| `--fs-link-small-text-size` | `var(--fs-text-size-1)` |
| `--fs-link-min-width` | `auto` |
| `--fs-link-min-height` | `var(--fs-link-min-width)` |
| `--fs-link-border-radius` | `var(--fs-border-radius)` |
| `--fs-link-text-color` | `var(--fs-color-link)` |
| `--fs-link-text-color-visited` | `var(--fs-color-link-visited)` |
| `--fs-link-text-decoration` | `none` |
| `--fs-link-text-decoration-hover` | `underline` |
| `--fs-link-text-line-height` | `1.5` |
| `--fs-link-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-link-transition-property` | `var(--fs-transition-property)` |
| `--fs-link-transition-function` | `var(--fs-transition-function)` |
| **Inline variant** | |
| `--fs-link-inline-padding` | `0` |
| `--fs-link-inline-text-color` | `var(--fs-link-text-color)` |
| `--fs-link-inline-text-decoration` | `underline` |
| **Display variant** | |
| `--fs-link-display-text-color` | `var(--fs-color-text-display)` |
| `--fs-link-display-text-color-visited` | `var(--fs-link-display-text-color)` |
| `--fs-link-display-text-line-height` | `var(--fs-link-text-line-height)` |
| **Inverse variant** | |
| `--fs-link-inverse-text-color` | `var(--fs-color-link-inverse)` |
| `--fs-link-inverse-text-color-visited` | `var(--fs-link-inverse-text-color)` |
---
### List
| Variable | Default Value |
|---|---|
| `--fs-list-style-ordered` | `decimal` |
| `--fs-list-style-unordered` | `initial` |
---
### Loader
| Variable | Default Value |
|---|---|
| `--fs-loader-gap` | `var(--fs-spacing-0)` |
| `--fs-loader-item-width` | `var(--fs-spacing-0)` |
| `--fs-loader-item-height` | `var(--fs-loader-item-width)` |
| `--fs-loader-item-border-radius` | `var(--fs-border-radius-circle)` |
| `--fs-loader-item-initial-opacity` | `.6` |
| `--fs-loader-animation-timing` | `var(--fs-transition-timing)` |
| `--fs-loader-animation-function` | `var(--fs-transition-function)` |
| `--fs-loader-dark-item-bkg-color` | `var(--fs-color-primary-bkg-active)` |
| `--fs-loader-light-item-bkg-color` | `var(--fs-color-tertiary-bkg-light)` |
---
### Overlay
| Variable | Default Value |
|---|---|
| `--fs-overlay-bkg-color` | `rgb(0 0 0 / 20%)` |
---
### Price
| Variable | Default Value |
|---|---|
| `--fs-price-spot-color` | `var(--fs-color-text)` |
| `--fs-price-spot-font-weight` | `var(--fs-text-weight-bold)` |
| `--fs-price-listing-color` | `var(--fs-color-text-light)` |
| `--fs-price-listing-text-decoration` | `line-through` |
| `--fs-price-listing-text-size` | `var(--fs-text-size-legend)` |
---
### Radio
| Variable | Default Value |
|---|---|
| `--fs-radio-width` | `1.25rem` |
| `--fs-radio-height` | `var(--fs-radio-width)` |
| `--fs-radio-border-radius` | `var(--fs-border-radius-circle)` |
| `--fs-radio-border-color` | `var(--fs-border-color)` |
| `--fs-radio-border-color-hover` | `var(--fs-border-color-hover)` |
| `--fs-radio-border-width` | `var(--fs-border-width)` |
| `--fs-radio-bkg-color-hover` | `var(--fs-color-primary-bkg-light)` |
| `--fs-radio-shadow-hover` | `0 0 0 var(--fs-radio-border-width) var(--fs-border-color-active)` |
| `--fs-radio-transition` | `border, background-color, box-shadow` |
| `--fs-radio-field-gap` | `var(--fs-spacing-1)` |
| `--fs-radio-knob-bkg-color` | `var(--fs-color-body-bkg)` |
| `--fs-radio-knob-width` | `var(--fs-spacing-1)` |
| `--fs-radio-knob-height` | `var(--fs-radio-knob-width)` |
| **Checked** | |
| `--fs-radio-checked-bkg-color` | `var(--fs-color-primary-bkg)` |
| `--fs-radio-checked-bkg-color-hover` | `var(--fs-color-primary-bkg-hover)` |
| **Disabled** | |
| `--fs-radio-disabled-bkg-color` | `var(--fs-color-disabled-bkg)` |
| `--fs-radio-disabled-border-color` | `var(--fs-border-color-disabled)` |
| `--fs-radio-disabled-border-width` | `var(--fs-radio-border-width)` |
| `--fs-radio-disabled-text-color` | `var(--fs-color-disabled-text)` |
| `--fs-radio-knob-disabled-bkg-color` | `var(--fs-color-neutral-5)` |
---
### Select
| Variable | Default Value |
|---|---|
| `--fs-select-height` | `var(--fs-spacing-6)` |
| `--fs-select-min-height` | `var(--fs-control-tap-size)` |
| `--fs-select-padding` | `var(--fs-spacing-1) var(--fs-spacing-5) var(--fs-spacing-1) var(--fs-spacing-2)` |
| `--fs-select-border-radius` | `var(--fs-border-radius)` |
| `--fs-select-bkg` | `transparent` |
| `--fs-select-bkg-color-hover` | `var(--fs-select-bkg-color-focus)` |
| `--fs-select-bkg-color-focus` | `var(--fs-color-primary-bkg-light)` |
| `--fs-select-text-color` | `var(--fs-color-link)` |
| `--fs-select-icon-color` | `var(--fs-color-link)` |
| `--fs-select-icon-width` | `var(--fs-spacing-3)` |
| `--fs-select-icon-height` | `var(--fs-select-icon-width)` |
| `--fs-select-icon-position-right` | `var(--fs-spacing-1)` |
| `--fs-select-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-select-transition-property` | `var(--fs-transition-property)` |
| `--fs-select-transition-function` | `var(--fs-transition-function)` |
| `--fs-select-disabled-text-color` | `var(--fs-color-disabled-text)` |
| `--fs-select-disabled-text-opacity` | `1` |
| **Field** | |
| `--fs-select-field-label-color` | `var(--fs-color-text-light)` |
| `--fs-select-field-label-margin-right` | `var(--fs-spacing-1)` |
---
### Skeleton
| Variable | Default Value |
|---|---|
| `--fs-skeleton-bkg-color` | `var(--fs-color-disabled-bkg)` |
| `--fs-skeleton-border-radius` | `var(--fs-border-radius)` |
| `--fs-skeleton-shimmer-bkg-color` | `rgb(255 255 255 / 20%)` |
| `--fs-skeleton-shimmer-box-shadow` | `0 0 var(--fs-spacing-5) var(--fs-spacing-5) var(--fs-skeleton-shimmer-bkg-color)` |
| `--fs-skeleton-shimmer-height` | `100%` |
| `--fs-skeleton-shimmer-width` | `50%` |
| `--fs-skeleton-shimmer-transition-timing` | `850ms` |
| `--fs-skeleton-shimmer-transition-iteration` | `infinite` |
| `--fs-skeleton-shimmer-transition-function` | `linear` |
---
### Slider
| Variable | Default Value |
|---|---|
| `--fs-slider-height` | `var(--fs-spacing-2)` |
| `--fs-slider-border-radius` | `var(--fs-border-radius-pill)` |
| `--fs-slider-margin-bottom` | `var(--fs-spacing-3)` |
| `--fs-slider-bkg-color` | `var(--fs-color-neutral-bkg)` |
| `--fs-slider-selection-bkg-color` | `var(--fs-color-primary-bkg-light-active)` |
| `--fs-slider-absolute-values-text-color` | `var(--fs-color-disabled-text)` |
| `--fs-slider-thumb-size` | `var(--fs-spacing-4)` |
| `--fs-slider-thumb-bkg-color` | `var(--fs-color-primary-bkg)` |
| `--fs-slider-thumb-bkg-color-hover` | `var(--fs-color-primary-bkg-hover)` |
| `--fs-slider-thumb-border-color` | `var(--fs-slider-thumb-bkg-color)` |
| `--fs-slider-thumb-border-color-hover` | `var(--fs-slider-thumb-bkg-color-hover)` |
| `--fs-slider-thumb-border-radius` | `var(--fs-border-radius-circle)` |
| `--fs-slider-thumb-border-width` | `var(--fs-border-width)` |
| `--fs-slider-value-label-bkg-color` | `var(--fs-color-body-bkg)` |
| `--fs-slider-value-label-bottom` | `var(--fs-spacing-3)` |
| `--fs-slider-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-slider-transition-property` | `var(--fs-transition-property)` |
| `--fs-slider-transition-function` | `var(--fs-transition-function)` |
---
### Textarea
| Variable | Default Value |
|---|---|
| `--fs-textarea-height` | `calc(var(--fs-control-tap-size) * 3)` |
| `--fs-textarea-width` | `100%` |
| `--fs-textarea-padding` | `var(--fs-spacing-1) var(--fs-spacing-2)` |
| `--fs-textarea-line-height` | `1.25` |
| `--fs-textarea-bkg-color` | `var(--fs-color-body-bkg)` |
| `--fs-textarea-border-color` | `var(--fs-border-color)` |
| `--fs-textarea-border-color-hover` | `var(--fs-border-color-active)` |
| `--fs-textarea-border-radius` | `var(--fs-border-radius)` |
| `--fs-textarea-border-width` | `var(--fs-border-width)` |
| `--fs-textarea-box-shadow` | `none` |
| `--fs-textarea-box-shadow-hover` | `0 0 0 var(--fs-border-width) var(--fs-border-color-active)` |
| `--fs-textarea-text-color` | `var(--fs-color-text)` |
| `--fs-textarea-text-size` | `var(--fs-text-size-body)` |
| `--fs-textarea-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-textarea-transition-property` | `var(--fs-transition-property)` |
| `--fs-textarea-transition-function` | `var(--fs-transition-function)` |
| **Disabled** | |
| `--fs-textarea-disabled-bkg-color` | `var(--fs-color-disabled-bkg)` |
| `--fs-textarea-disabled-border-color` | `var(--fs-border-color)` |
| `--fs-textarea-disabled-border-width` | `var(--fs-border-width)` |
| `--fs-textarea-disabled-text-color` | `var(--fs-color-disabled-text)` |
---
## Molecules
### Accordion
| Variable | Default Value |
|---|---|
| `--fs-accordion-button-padding` | `var(--fs-spacing-3) 0` |
| `--fs-accordion-button-bkg-color` | `transparent` |
| `--fs-accordion-button-color` | `var(--fs-color-text)` |
| `--fs-accordion-button-font-size` | `var(--fs-text-size-3)` |
| `--fs-accordion-button-font-weight` | `var(--fs-text-weight-bold)` |
| `--fs-accordion-button-line-height` | `1.2` |
| `--fs-accordion-item-border-bottom-color` | `var(--fs-border-color-light)` |
| `--fs-accordion-item-border-bottom-width` | `var(--fs-border-width)` |
| `--fs-accordion-panel-padding-bottom` | `var(--fs-spacing-4)` |
---
### Alert
| Variable | Default Value |
|---|---|
| `--fs-alert-height` | `var(--fs-spacing-7)` |
| `--fs-alert-padding-left` | `var(--fs-spacing-3)` |
| `--fs-alert-padding-right` | `var(--fs-alert-padding-left)` |
| `--fs-alert-bkg-color` | `var(--fs-color-highlighted-bkg)` |
| `--fs-alert-text-color` | `var(--fs-color-highlighted-text)` |
| `--fs-alert-text-size` | `var(--fs-text-size-1)` |
| `--fs-alert-icon-color` | `var(--fs-alert-text-color)` |
| `--fs-alert-icon-width` | `var(--fs-spacing-4)` |
| `--fs-alert-icon-height` | `var(--fs-alert-icon-width)` |
| `--fs-alert-icon-margin-right` | `var(--fs-spacing-1)` |
| `--fs-alert-link-color` | `var(--fs-alert-text-color)` |
| `--fs-alert-link-color-visited` | `var(--fs-alert-text-color)` |
| `--fs-alert-button-bkg-color` | `var(--fs-alert-bkg-color)` |
| `--fs-alert-button-border-radius` | `var(--fs-border-radius)` |
| `--fs-alert-button-text-color` | `var(--fs-alert-text-color)` |
---
### Breadcrumb
| Variable | Default Value |
|---|---|
| `--fs-breadcrumb-padding` | `var(--fs-spacing-2) 0` |
| `--fs-breadcrumb-margin-left` | `var(--fs-spacing-0)` |
| `--fs-breadcrumb-divider-height` | `var(--fs-spacing-3)` |
| `--fs-breadcrumb-divider-margin` | `var(--fs-spacing-1)` |
| `--fs-breadcrumb-divider-border-left-color` | `var(--fs-border-color-light)` |
| `--fs-breadcrumb-divider-border-left-width` | `var(--fs-border-width)` |
| `--fs-breadcrumb-list-item-padding` | `var(--fs-spacing-0)` |
| `--fs-breadcrumb-list-item-max-width-mobile` | `30%` |
| `--fs-breadcrumb-list-item-last-text-color` | `var(--fs-color-text-light)` |
| `--fs-breadcrumb-link-home-color` | `var(--fs-color-text)` |
| `--fs-breadcrumb-link-home-padding` | `var(--fs-spacing-1)` |
| `--fs-breadcrumb-link-home-border-radius` | `var(--fs-border-radius-circle)` |
| `--fs-breadcrumb-link-home-hover-bkg-color` | `var(--fs-color-primary-bkg-light)` |
| `--fs-breadcrumb-link-color-visited` | `var(--fs-color-link)` |
| `--fs-breadcrumb-dropdown-button-color` | `var(--fs-color-link)` |
| `--fs-breadcrumb-dropdown-button-border-radius` | `var(--fs-spacing-0)` |
| `--fs-breadcrumb-dropdown-button-margin-left` | `var(--fs-breadcrumb-margin-left)` |
| `--fs-breadcrumb-dropdown-button-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-breadcrumb-dropdown-button-transition-property` | `var(--fs-transition-property)` |
| `--fs-breadcrumb-dropdown-button-transition-function` | `var(--fs-transition-function)` |
---
### BuyButton
| Variable | Default Value |
|---|---|
| `--fs-buy-button-bkg-color` | `var(--fs-color-action-bkg)` |
| `--fs-buy-button-bkg-color-hover` | `var(--fs-color-action-bkg-hover)` |
| `--fs-buy-button-bkg-color-active` | `var(--fs-color-action-bkg-active)` |
| `--fs-buy-button-border-color` | `var(--fs-buy-button-bkg-color)` |
| `--fs-buy-button-border-color-hover` | `var(--fs-buy-button-bkg-color-hover)` |
| `--fs-buy-button-border-color-active` | `var(--fs-buy-button-bkg-color-active)` |
| `--fs-buy-button-text-color` | `var(--fs-color-action-text)` |
| `--fs-buy-button-text-color-hover` | `var(--fs-color-action-text)` |
| `--fs-buy-button-text-color-active` | `var(--fs-color-action-text)` |
| `--fs-buy-button-shadow-hover` | `var(--fs-button-shadow-hover)` |
---
### Card
| Variable | Default Value |
|---|---|
| `--fs-card-border-radius` | `var(--fs-border-radius)` |
| `--fs-card-border-color` | `var(--fs-border-color-light)` |
| `--fs-card-border-width` | `var(--fs-border-width)` |
| `--fs-card-body-padding` | `var(--fs-spacing-3)` |
| `--fs-card-header-bkg-color` | `var(--fs-color-neutral-bkg)` |
| `--fs-card-header-padding` | `var(--fs-spacing-3)` |
| `--fs-card-header-font-weight` | `var(--fs-text-weight-bold)` |
| `--fs-card-header-icon-color` | `var(--fs-color-main-2)` |
---
### Carousel
| Variable | Default Value |
|---|---|
| `--fs-carousel-padding-mobile` | `var(--fs-spacing-0) var(--fs-grid-padding)` |
| `--fs-carousel-padding-desktop` | `var(--fs-spacing-0) calc((100% - var(--fs-grid-max-width)) / 2) var(--fs-spacing-0)` |
| `--fs-carousel-item-margin-right` | `var(--fs-spacing-3)` |
| **Controls** | |
| `--fs-carousel-controls-width` | `3.125rem` |
| `--fs-carousel-controls-height` | `var(--fs-carousel-controls-width)` |
| `--fs-carousel-controls-bkg-color` | `var(--fs-color-neutral-0)` |
| `--fs-carousel-controls-border-radius` | `var(--fs-border-radius-circle)` |
| `--fs-carousel-controls-box-shadow` | `var(--fs-shadow-darker)` |
| `--fs-carousel-controls-icon-color` | `var(--fs-color-neutral-7)` |
| `--fs-carousel-controls-control-left` | `var(--fs-spacing-4)` |
| `--fs-carousel-controls-control-right` | `var(--fs-carousel-controls-control-left)` |
| `--fs-carousel-controls-control-max-left` | `calc(-1 * var(--fs-spacing-11))` |
| `--fs-carousel-controls-control-max-right` | `var(--fs-carousel-controls-control-max-left)` |
| **Bullets** | |
| `--fs-carousel-bullet-bkg-color` | `var(--fs-color-neutral-3)` |
| `--fs-carousel-bullet-bkg-color-selected` | `var(--fs-color-main-4)` |
| `--fs-carousel-bullet-border-radius` | `var(--fs-carousel-controls-border-radius)` |
| `--fs-carousel-bullet-width-mobile` | `100%` |
| `--fs-carousel-bullet-height-mobile` | `var(--fs-spacing-0)` |
| `--fs-carousel-bullet-width-desktop` | `var(--fs-spacing-1)` |
| `--fs-carousel-bullet-height-desktop` | `var(--fs-carousel-bullet-width-desktop)` |
| `--fs-carousel-bullets-padding-top` | `var(--fs-carousel-controls-control-left)` |
| `--fs-carousel-bullets-padding-left` | `var(--fs-grid-padding)` |
| `--fs-carousel-bullets-padding-right` | `var(--fs-carousel-bullets-padding-left)` |
| `--fs-carousel-bullets-column-gap-mobile` | `var(--fs-spacing-0)` |
| `--fs-carousel-bullets-column-gap-tablet` | `var(--fs-spacing-3)` |
---
### CartItem
| Variable | Default Value |
|---|---|
| `--fs-cart-item-padding` | `var(--fs-spacing-2)` |
| `--fs-cart-item-bkg-color` | `var(--fs-control-bkg)` |
| `--fs-cart-item-border-radius` | `var(--fs-border-radius)` |
| `--fs-cart-item-border-color` | `var(--fs-border-color-light)` |
| `--fs-cart-item-border-width` | `var(--fs-border-width)` |
| `--fs-cart-item-image-width` | `var(--fs-spacing-8)` |
| `--fs-cart-item-image-height` | `var(--fs-cart-item-image-width)` |
| `--fs-cart-item-image-border-radius` | `var(--fs-cart-item-border-radius)` |
| `--fs-cart-item-title-color` | `var(--fs-color-text)` |
| `--fs-cart-item-title-weight` | `var(--fs-text-weight-bold)` |
| `--fs-cart-item-title-line-height` | `1.2` |
| `--fs-cart-item-skus-text-color` | `var(--fs-color-text-light)` |
| `--fs-cart-item-skus-text-size` | `var(--fs-text-size-legend)` |
| `--fs-cart-item-skus-line-height` | `var(--fs-text-size-body)` |
| `--fs-cart-item-skus-margin-top` | `var(--fs-spacing-0)` |
| `--fs-cart-item-skus-row-gap` | `var(--fs-spacing-0)` |
| `--fs-cart-item-skus-column-gap` | `var(--fs-spacing-1)` |
---
### DiscountBadge
| Variable | Default Value |
|---|---|
| `--fs-discount-badge-low-bkg-color` | `var(--fs-badge-success-bkg-color)` |
| `--fs-discount-badge-low-border-color` | `var(--fs-badge-success-border-color)` |
| `--fs-discount-badge-low-text-color` | `var(--fs-badge-success-text-color)` |
| `--fs-discount-badge-medium-bkg-color` | `var(--fs-badge-warning-bkg-color)` |
| `--fs-discount-badge-medium-border-color` | `var(--fs-badge-warning-border-color)` |
| `--fs-discount-badge-medium-text-color` | `var(--fs-badge-warning-text-color)` |
| `--fs-discount-badge-high-bkg-color` | `var(--fs-badge-danger-bkg-color)` |
| `--fs-discount-badge-high-border-color` | `var(--fs-badge-danger-border-color)` |
| `--fs-discount-badge-high-text-color` | `var(--fs-badge-danger-text-color)` |
---
### Dropdown
| Variable | Default Value |
|---|---|
| **Menu** | |
| `--fs-dropdown-menu-bkg-color` | `var(--fs-color-tertiary-bkg)` |
| `--fs-dropdown-menu-border-radius` | `var(--fs-border-radius)` |
| `--fs-dropdown-menu-box-shadow` | `var(--fs-shadow-hover)` |
| **Item** | |
| `--fs-dropdown-item-bkg-color` | `var(--fs-color-tertiary-bkg-light)` |
| `--fs-dropdown-item-bkg-color-hover` | `var(--fs-color-primary-bkg-light)` |
| `--fs-dropdown-item-border-bottom-color` | `var(--fs-border-color-light)` |
| `--fs-dropdown-item-color` | `var(--fs-color-link)` |
| `--fs-dropdown-item-min-height` | `2.375rem` |
| `--fs-dropdown-item-padding` | `var(--fs-spacing-1) var(--fs-spacing-2) var(--fs-spacing-1) var(--fs-spacing-1)` |
| `--fs-dropdown-item-text-size` | `var(--fs-text-size-base)` |
| `--fs-dropdown-item-text-weight` | `var(--fs-text-weight-regular)` |
| `--fs-dropdown-item-icon-margin-right` | `var(--fs-spacing-0)` |
| `--fs-dropdown-item-icon-margin-top` | `calc(-1 * var(--fs-spacing-1))` |
| `--fs-dropdown-item-icon-min-width` | `1.125rem` |
| `--fs-dropdown-item-small-min-height` | `1.75rem` |
| `--fs-dropdown-item-small-padding` | `var(--fs-spacing-0) var(--fs-spacing-2) var(--fs-spacing-0) var(--fs-spacing-1)` |
| `--fs-dropdown-item-small-text-size` | `var(--fs-text-size-1)` |
---
### Gift
| Variable | Default Value |
|---|---|
| `--fs-gift-height` | `var(--fs-spacing-12)` |
| `--fs-gift-bkg-color` | `var(--fs-control-bkg)` |
| `--fs-gift-border-radius` | `var(--fs-border-radius)` |
| `--fs-gift-border-color` | `var(--fs-border-color-light)` |
| `--fs-gift-border-width` | `var(--fs-border-width)` |
| `--fs-gift-content-padding` | `var(--fs-spacing-1) var(--fs-spacing-2)` |
| `--fs-gift-content-row-gap` | `var(--fs-spacing-0)` |
| `--fs-gift-icon-bkg-color` | `var(--fs-color-body-bkg)` |
| `--fs-gift-icon-color` | `var(--fs-gift-title-color)` |
| `--fs-gift-icon-size` | `1.5rem` |
| `--fs-gift-icon-padding` | `var(--fs-spacing-0)` |
| `--fs-gift-title-color` | `var(--fs-color-text)` |
| `--fs-gift-title-size` | `var(--fs-text-size-body)` |
| `--fs-gift-title-line-height` | `1.25` |
| `--fs-gift-price-size` | `var(--fs-text-size-legend)` |
---
### InputField
| Variable | Default Value |
|---|---|
| `--fs-input-field-color` | `var(--fs-color-text)` |
| `--fs-input-field-size` | `var(--fs-text-size-body)` |
| `--fs-input-field-padding` | `var(--fs-spacing-2) var(--fs-spacing-2) 0` |
| `--fs-input-field-border-color` | `var(--fs-border-color)` |
| `--fs-input-field-button-height` | `var(--fs-control-tap-size)` |
| `--fs-input-field-label-color` | `var(--fs-color-text-light)` |
| `--fs-input-field-label-size` | `var(--fs-text-size-tiny)` |
| `--fs-input-field-label-padding` | `0 var(--fs-spacing-2)` |
| `--fs-input-field-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-input-field-transition-property` | `var(--fs-transition-property)` |
| `--fs-input-field-transition-function` | `var(--fs-transition-function)` |
| **Disabled** | |
| `--fs-input-field-disabled-bkg-color` | `var(--fs-color-disabled-bkg)` |
| `--fs-input-field-disabled-border-color` | `var(--fs-border-color)` |
| `--fs-input-field-disabled-border-width` | `var(--fs-border-width)` |
| `--fs-input-field-disabled-text-color` | `var(--fs-color-disabled-text)` |
| **Error** | |
| `--fs-input-field-error-border-color` | `var(--fs-color-danger-border)` |
| `--fs-input-field-error-box-shadow` | `0 0 0 var(--fs-border-width) var(--fs-input-field-error-border-color)` |
| `--fs-input-field-error-focus-ring` | `var(--fs-color-focus-ring-danger)` |
| `--fs-input-field-error-message-color` | `var(--fs-color-danger-text)` |
| `--fs-input-field-error-message-size` | `var(--fs-text-size-legend)` |
| `--fs-input-field-error-message-line-height` | `1.1` |
| `--fs-input-field-error-message-margin-top` | `var(--fs-spacing-0)` |
---
### Modal
| Variable | Default Value |
|---|---|
| `--fs-modal-background-color` | `var(--fs-color-body-bkg)` |
| `--fs-modal-border-radius` | `var(--fs-border-radius)` |
| `--fs-modal-margin` | `auto` |
| `--fs-modal-min-height` | `var(--fs-spacing-5)` |
| `--fs-modal-max-width` | `calc(var(--fs-grid-breakpoint-desktop) / 3)` |
| `--fs-modal-width-tablet` | `calc(100vw / 3)` |
| `--fs-modal-min-width-tablet` | `calc(var(--fs-grid-breakpoint-desktop) / 3)` |
| `--fs-modal-position-top` | `30vh` |
| `--fs-modal-position-left` | `var(--fs-spacing-4)` |
| `--fs-modal-position-right` | `var(--fs-spacing-4)` |
| `--fs-modal-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-modal-transition-property` | `transform` |
| `--fs-modal-transition-in-function` | `ease-in` |
| `--fs-modal-transition-out-function` | `ease-in` |
| **Header** | |
| `--fs-modal-header-padding` | `var(--fs-spacing-4) var(--fs-spacing-7) var(--fs-spacing-4) var(--fs-spacing-4)` |
| `--fs-modal-header-title-size` | `var(--fs-text-size-lead)` |
| `--fs-modal-header-title-weight` | `var(--fs-text-weight-bold)` |
| `--fs-modal-header-title-line-height` | `1.2` |
| `--fs-modal-header-title-margin-bottom` | `.625rem` |
| `--fs-modal-header-description-color` | `var(--fs-color-text-light)` |
| `--fs-modal-header-description-size` | `var(--fs-text-size-body)` |
| `--fs-modal-header-description-line-height` | `1.5` |
| `--fs-modal-header-close-button-position-top` | `0` |
| `--fs-modal-header-close-button-position-right` | `0` |
| **Body** | |
| `--fs-modal-body-padding` | `var(--fs-spacing-1) var(--fs-spacing-4) var(--fs-spacing-5)` |
| **Footer** | |
| `--fs-modal-footer-padding` | `var(--fs-spacing-3) 0 var(--fs-spacing-3)` |
| `--fs-modal-footer-box-shadow` | `0 -1px 15px 0 rgb(0 0 0 / 10.2%)` |
| `--fs-modal-footer-actions-gap` | `var(--fs-spacing-3)` |
| `--fs-modal-footer-actions-padding` | `var(--fs-spacing-1) var(--fs-spacing-4)` |
---
### NavbarLinks
| Variable | Default Value |
|---|---|
| `--fs-navbar-links-bkg-color` | `var(--fs-color-body-bkg)` |
| `--fs-navbar-links-border-top-color-mobile` | `var(--fs-border-color-light)` |
| `--fs-navbar-links-border-top-width-mobile` | `var(--fs-border-width)` |
| `--fs-navbar-links-border-bottom-color-mobile` | `var(--fs-navbar-links-border-top-color-mobile)` |
| `--fs-navbar-links-border-bottom-width-mobile` | `var(--fs-navbar-links-border-top-width-mobile)` |
| `--fs-navbar-links-link-padding-notebook` | `0 var(--fs-spacing-0)` |
| `--fs-navbar-links-link-width-notebook` | `auto` |
| `--fs-navbar-links-list-margin-left-notebook` | `var(--fs-spacing-2)` |
| `--fs-navbar-links-list-padding-left-notebook` | `var(--fs-spacing-3)` |
| `--fs-navbar-links-list-border-left-color-notebook` | `var(--fs-border-color-light)` |
| `--fs-navbar-links-list-border-left-width-notebook` | `var(--fs-border-width)` |
| `--fs-navbar-links-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-navbar-links-transition-property` | `var(--fs-transition-property)` |
| `--fs-navbar-links-transition-function` | `var(--fs-transition-function)` |
---
### OrderSummary
| Variable | Default Value |
|---|---|
| `--fs-order-summary-padding` | `var(--fs-spacing-3)` |
| `--fs-order-summary-margin-bottom` | `var(--fs-spacing-2)` |
| `--fs-order-summary-row-gap` | `0` |
| `--fs-order-summary-discount-text-color` | `var(--fs-color-success-text)` |
| `--fs-order-summary-taxes-label-color` | `var(--fs-color-info-text)` |
| `--fs-order-summary-taxes-text-size` | `var(--fs-text-size-tiny)` |
| `--fs-order-summary-taxes-text-weight` | `var(--fs-text-weight-regular)` |
| `--fs-order-summary-total-text-size` | `var(--fs-text-size-3)` |
| `--fs-order-summary-total-text-font-weight` | `var(--fs-text-weight-bold)` |
---
### PickupPointCard
| Variable | Default Value |
|---|---|
| `--fs-pickup-point-card-height` | `140px` |
| `--fs-pickup-point-card-padding` | `var(--fs-spacing-3)` |
| `--fs-pickup-point-card-border-radius` | `var(--fs-border-radius)` |
| `--fs-pickup-point-card-border-color` | `var(--fs-border-color-light)` |
| `--fs-pickup-point-card-border-width` | `var(--fs-border-width)` |
| `--fs-pickup-point-card-row-gap` | `var(--fs-grid-gap-2)` |
| `--fs-pickup-point-card-header-title-font-weight` | `var(--fs-text-weight-medium)` |
| `--fs-pickup-point-card-header-icon-color` | `var(--fs-border-color-disabled)` |
| `--fs-pickup-point-card-distance-color` | `var(--fs-color-text-light)` |
| `--fs-pickup-point-card-distance-font-size` | `var(--fs-text-size-legend)` |
---
### Popover
| Variable | Default Value |
|---|---|
| `--fs-popover-margin` | `0 var(--fs-spacing-3)` |
| `--fs-popover-padding` | `var(--fs-spacing-3) var(--fs-spacing-4) var(--fs-spacing-4)` |
| `--fs-popover-padding-inline` | `var(--fs-spacing-4)` |
| `--fs-popover-border-radius` | `var(--fs-border-radius)` |
| `--fs-popover-bkg-color` | `var(--fs-color-body-bkg)` |
| `--fs-popover-box-shadow` | `var(--fs-shadow-darker)` |
| `--fs-popover-z-index` | `var(--fs-z-index-top)` |
| `--fs-popover-indicator-size` | `var(--fs-spacing-1)` |
| `--fs-popover-indicator-distance-base` | `var(--fs-spacing-1)` |
| `--fs-popover-indicator-distance-edge` | `var(--fs-spacing-3)` |
| `--fs-popover-indicator-translate` | `calc(--fs-popover-indicator-size + --fs-popover-indicator-distance-base)` |
---
### ProductCard
| Variable | Default Value |
|---|---|
| `--fs-product-card-min-width` | `10rem` |
| `--fs-product-card-padding` | `var(--fs-spacing-1) var(--fs-spacing-1) var(--fs-spacing-2) var(--fs-spacing-1)` |
| `--fs-product-card-gap` | `var(--fs-spacing-2)` |
| `--fs-product-card-bkg-color` | `var(--fs-color-body-bkg)` |
| `--fs-product-card-bkg-color-hover` | `var(--fs-product-card-bkg-color)` |
| `--fs-product-card-bkg-color-focus` | `var(--fs-product-card-bkg-color-hover)` |
| `--fs-product-card-border-radius` | `var(--fs-border-radius)` |
| `--fs-product-card-border-color` | `var(--fs-border-color-light)` |
| `--fs-product-card-border-color-hover` | `var(--fs-border-color-hover)` |
| `--fs-product-card-border-width` | `var(--fs-border-width)` |
| `--fs-product-card-shadow` | `var(--fs-shadow)` |
| `--fs-product-card-shadow-hover` | `var(--fs-shadow-hover)` |
| `--fs-product-card-img-radius` | `var(--fs-product-card-border-radius)` |
| `--fs-product-card-img-scale-hover` | `1` |
| `--fs-product-card-title-color` | `var(--fs-color-text)` |
| `--fs-product-card-title-size` | `var(--fs-text-size-base)` |
| `--fs-product-card-title-weight` | `var(--fs-text-weight-regular)` |
| `--fs-product-card-title-max-lines` | `var(--fs-text-max-lines)` |
| `--fs-product-card-price-color` | `var(--fs-color-text)` |
| `--fs-product-card-price-size` | `var(--fs-text-size-base)` |
| `--fs-product-card-sponsored-label-color` | `var(--fs-color-text-light)` |
| `--fs-product-card-sponsored-label-size` | `var(--fs-text-size-tiny)` |
| `--fs-product-card-taxes-label-color` | `var(--fs-color-info-text)` |
| `--fs-product-card-taxes-text-size` | `var(--fs-text-size-tiny)` |
| `--fs-product-card-taxes-text-weight` | `var(--fs-text-weight-regular)` |
| `--fs-product-card-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-product-card-transition-property` | `var(--fs-transition-property)` |
| `--fs-product-card-transition-function` | `var(--fs-transition-function)` |
| **Wide variant** | |
| `--fs-product-card-wide-min-width` | `9rem` |
| `--fs-product-card-wide-padding` | `0` |
| `--fs-product-card-wide-bkg-color` | `var(--fs-color-neutral-bkg)` |
| `--fs-product-card-wide-content-padding` | `var(--fs-spacing-2)` |
| **Out of stock** | |
| `--fs-product-card-out-of-stock-bkg-color` | `transparent` |
| `--fs-product-card-out-of-stock-border-color` | `var(--fs-color-neutral-1)` |
| `--fs-product-card-out-of-stock-img-opacity` | `.5` |
| **Delivery Promise** | |
| `--fs-product-card-delivery-promise-badge-text-size` | `var(--fs-text-size-tiny)` |
| `--fs-product-card-delivery-promise-badge-status-width` | `var(--fs-spacing-1)` |
| `--fs-product-card-delivery-promise-badge-status-border-radius` | `var(--fs-border-radius-circle)` |
| `--fs-product-card-delivery-promise-badge-status-available` | `var(--fs-color-success-2)` |
| `--fs-product-card-delivery-promise-badge-status-unavailable` | `var(--fs-color-neutral-bkg)` |
---
### ProductCardSkeleton
| Variable | Default Value |
|---|---|
| `--fs-product-card-skeleton-border-radius` | `var(--fs-border-radius)` |
| `--fs-product-card-skeleton-bordered` | `var(--fs-border-width) solid var(--fs-border-color-light)` |
| `--fs-product-card-skeleton-gap` | `var(--fs-spacing-1)` |
| `--fs-product-card-skeleton-padding` | `var(--fs-spacing-1) var(--fs-spacing-1) var(--fs-spacing-2)` |
| `--fs-product-card-skeleton-sectioned-min-width` | `10rem` |
---
### ProductPrice
| Variable | Default Value |
|---|---|
| `--fs-product-price-gap` | `var(--fs-spacing-1)` |
---
### ProductTile
| Variable | Default Value |
|---|---|
| `--fs-product-tile-skeleton-gap` | `var(--fs-spacing-1)` |
| `--fs-product-tile-skeleton-content-padding` | `var(--fs-spacing-3)` |
| `--fs-product-tile-skeleton-wide-bkg-color` | `var(--fs-color-neutral-bkg)` |
| `--fs-product-tile-skeleton-wide-padding` | `var(--fs-spacing-2) var(--fs-spacing-3) var(--fs-spacing-3)` |
---
### ProductTitle
| Variable | Default Value |
|---|---|
| `--fs-product-title-text-size` | `var(--fs-text-size-title-product)` |
| `--fs-product-title-text-weight` | `var(--fs-text-weight-regular)` |
| `--fs-product-title-line-height` | `1.12` |
| `--fs-product-title-column-gap` | `var(--fs-spacing-2)` |
| `--fs-product-title-row-gap` | `var(--fs-spacing-3)` |
| `--fs-product-title-addendum-color` | `var(--fs-color-text-light)` |
| `--fs-product-title-addendum-size` | `var(--fs-text-size-1)` |
| `--fs-product-title-addendum-line-height` | `1.7` |
---
### QuantitySelector
| Variable | Default Value |
|---|---|
| `--fs-qty-selector-width` | `calc(var(--fs-control-tap-size) * 2.7)` |
| `--fs-qty-selector-height` | `calc(var(--fs-control-tap-size) + (var(--fs-qty-selector-border-width) * 2))` |
| `--fs-qty-selector-border-radius` | `var(--fs-border-radius)` |
| `--fs-qty-selector-border-color` | `var(--fs-border-color)` |
| `--fs-qty-selector-border-color-hover` | `var(--fs-border-color-active)` |
| `--fs-qty-selector-border-width` | `var(--fs-border-width)` |
| `--fs-qty-selector-border-width-hover` | `var(--fs-border-width)` |
| `--fs-qty-selector-bkg-color` | `var(--fs-color-body-bkg)` |
| `--fs-qty-selector-bkg-color-hover` | `var(--fs-qty-selector-bkg-color)` |
| `--fs-qty-selector-button-bkg-color` | `transparent` |
| `--fs-qty-selector-button-border-radius` | `var(--fs-qty-selector-border-radius)` |
| `--fs-qty-selector-text-color` | `var(--fs-color-text)` |
| `--fs-qty-selector-text-size` | `var(--fs-text-size-base)` |
| `--fs-qty-selector-shadow` | `none` |
| `--fs-qty-selector-shadow-hover` | `0 0 0 var(--fs-border-width) var(--fs-border-color-active)` |
| `--fs-qty-selector-disabled-bkg-color` | `var(--fs-color-disabled-bkg)` |
| `--fs-qty-selector-disabled-border-color` | `var(--fs-qty-selector-disabled-bkg-color)` |
| `--fs-qty-selector-disabled-text-color` | `var(--fs-color-disabled-text)` |
---
### Rating
| Variable | Default Value |
|---|---|
| `--fs-rating-color` | `var(--fs-color-main-2)` |
| `--fs-rating-color-empty` | `var(--fs-color-neutral-4)` |
| `--fs-rating-gap` | `var(--fs-spacing-0)` |
| `--fs-rating-icon-width` | `var(--fs-spacing-3)` |
| `--fs-rating-icon-height` | `var(--fs-rating-icon-width)` |
| `--fs-rating-button-min-height` | `var(--fs-spacing-5)` |
| **Actionable** | |
| `--fs-rating-actionable-gap` | `0` |
| `--fs-rating-actionable-icon-color` | `var(--fs-rating-color-empty)` |
| `--fs-rating-actionable-icon-color-selected` | `var(--fs-rating-color)` |
| `--fs-rating-actionable-icon-width` | `var(--fs-rating-icon-width)` |
| `--fs-rating-actionable-icon-height` | `var(--fs-rating-actionable-icon-width)` |
| **Field** | |
| `--fs-rating-field-label-color` | `var(--fs-color-text-light)` |
| `--fs-rating-field-label-size` | `var(--fs-text-size-2)` |
| `--fs-rating-field-label-line-height` | `var(--fs-text-size-4)` |
| `--fs-rating-field-error-message-color` | `var(--fs-color-danger-text)` |
| `--fs-rating-field-error-message-size` | `var(--fs-text-size-legend)` |
| `--fs-rating-field-error-message-line-height` | `1.1` |
---
### RegionBar
| Variable | Default Value |
|---|---|
| `--fs-region-bar-width` | `100%` |
| `--fs-region-bar-padding` | `var(--fs-spacing-0) 0 var(--fs-spacing-0) var(--fs-spacing-2)` |
| `--fs-region-bar-padding-inline-start` | `var(--fs-spacing-2)` |
| `--fs-region-bar-bkg-color` | `var(--fs-color-body-bkg)` |
| `--fs-region-bar-border-bottom-color` | `var(--fs-border-color-light)` |
| `--fs-region-bar-border-bottom-width` | `var(--fs-border-width)` |
| `--fs-region-bar-text-color` | `var(--fs-color-text-display)` |
| `--fs-region-bar-location-height` | `var(--fs-spacing-4)` |
| `--fs-region-bar-icon-margin-right` | `var(--fs-spacing-1)` |
| `--fs-region-bar-message-margin-right` | `auto` |
| `--fs-region-bar-postal-code-margin-right` | `auto` |
| `--fs-region-bar-cta-margin-left` | `auto` |
| `--fs-region-bar-cta-text-decoration` | `underline` |
---
### Search Components
**SearchAutoComplete**
| Variable | Default Value |
|---|---|
| `--fs-search-auto-complete-padding-top` | `var(--fs-spacing-2)` |
| `--fs-search-auto-complete-padding-bottom` | `var(--fs-search-auto-complete-padding-top)` |
| `--fs-search-auto-complete-padding-right` | `var(--fs-spacing-3)` |
| `--fs-search-auto-complete-padding-left` | `var(--fs-search-auto-complete-padding-right)` |
| `--fs-search-auto-complete-item-column-gap` | `var(--fs-spacing-1)` |
| `--fs-search-auto-complete-item-line-height` | `1.25` |
| `--fs-search-auto-complete-item-text-size` | `var(--fs-text-size-2)` |
| `--fs-search-auto-complete-item-bkg-color-hover` | `var(--fs-color-tertiary-bkg-hover)` |
| `--fs-search-auto-complete-item-icon-color` | `var(--fs-color-neutral-4)` |
| `--fs-search-auto-complete-item-icon-size` | `1.125rem` |
| `--fs-search-auto-complete-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-search-auto-complete-transition-property` | `var(--fs-transition-property)` |
| `--fs-search-auto-complete-transition-function` | `var(--fs-transition-function)` |
**SearchDropdown**
| Variable | Default Value |
|---|---|
| `--fs-search-dropdown-bkg-color` | `var(--fs-color-neutral-0)` |
| `--fs-search-dropdown-border-radius` | `0 0 var(--fs-border-radius) var(--fs-border-radius)` |
| `--fs-search-dropdown-border-color` | `var(--fs-border-color)` |
| `--fs-search-dropdown-border-width` | `var(--fs-border-width)` |
| `--fs-search-dropdown-box-shadow` | `var(--fs-shadow)` |
| `--fs-search-dropdown-section-border-color` | `var(--fs-border-color-light)` |
| `--fs-search-dropdown-width-mobile` | `100vw` |
| `--fs-search-dropdown-width-desktop` | `100%` |
| `--fs-search-dropdown-position-top-mobile` | `calc(var(--fs-search-dropdown-position-top-tablet) + 1px)` |
| `--fs-search-dropdown-position-top-tablet` | `calc(var(--fs-control-tap-size) + var(--fs-border-width))` |
| `--fs-search-dropdown-position-top-desktop` | `var(--fs-search-input-height-desktop)` |
| `--fs-search-dropdown-position-left-mobile` | `calc(-1 * var(--fs-control-tap-size))` |
| `--fs-search-dropdown-position-left-tablet` | `calc(var(--fs-search-dropdown-position-left-mobile) - var(--fs-spacing-1))` |
**SearchHistory**
| Variable | Default Value |
|---|---|
| `--fs-search-history-padding-top` | `var(--fs-spacing-2)` |
| `--fs-search-history-padding-bottom` | `var(--fs-search-history-padding-top)` |
| `--fs-search-history-padding-right` | `var(--fs-spacing-3)` |
| `--fs-search-history-padding-left` | `var(--fs-search-history-padding-right)` |
| `--fs-search-history-title-size` | `var(--fs-text-size-lead)` |
| `--fs-search-history-title-line-height` | `1.5` |
| `--fs-search-history-item-text-size` | `var(--fs-text-size-2)` |
| `--fs-search-history-item-line-height` | `1.25` |
| `--fs-search-history-item-column-gap` | `var(--fs-spacing-1)` |
| `--fs-search-history-item-bkg-color-hover` | `var(--fs-color-tertiary-bkg-hover)` |
| `--fs-search-history-item-icon-color` | `var(--fs-color-neutral-4)` |
| `--fs-search-history-item-icon-size` | `1.125rem` |
| `--fs-search-history-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-search-history-transition-property` | `var(--fs-transition-property)` |
| `--fs-search-history-transition-function` | `var(--fs-transition-function)` |
**SearchInputField**
| Variable | Default Value |
|---|---|
| `--fs-search-input-field-height-mobile` | `var(--fs-control-tap-size)` |
| `--fs-search-input-field-height-desktop` | `var(--fs-spacing-6)` |
| `--fs-search-input-field-input-bkg-color` | `var(--fs-color-body-bkg)` |
| `--fs-search-input-field-input-padding-right` | `var(--fs-spacing-7)` |
| `--fs-search-input-field-button-min-height` | `var(--fs-search-input-field-height-desktop)` |
| `--fs-search-input-field-button-padding-top-desktop` | `var(--fs-spacing-0)` |
| `--fs-search-input-field-button-padding-bottom-desktop` | `var(--fs-search-input-field-button-padding-top-desktop)` |
| `--fs-search-input-field-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-search-input-field-transition-function` | `ease` |
**SearchProducts**
| Variable | Default Value |
|---|---|
| `--fs-search-products-padding-top` | `var(--fs-spacing-2)` |
| `--fs-search-products-padding-bottom` | `var(--fs-search-products-padding-top)` |
| `--fs-search-products-padding-right` | `var(--fs-spacing-3)` |
| `--fs-search-products-padding-left` | `var(--fs-search-products-padding-right)` |
| `--fs-search-products-title-size` | `var(--fs-text-size-lead)` |
| `--fs-search-products-title-line-height` | `1.5` |
| `--fs-search-product-item-padding-top` | `var(--fs-spacing-1)` |
| `--fs-search-product-item-padding-bottom` | `var(--fs-search-product-item-padding-top)` |
| `--fs-search-product-item-image-size` | `3.5rem` |
| `--fs-search-product-item-image-border-radius` | `var(--fs-border-radius)` |
| `--fs-search-product-item-image-margin-right` | `var(--fs-spacing-3)` |
| `--fs-search-product-item-title-color` | `var(--fs-color-text)` |
| `--fs-search-product-item-title-size` | `var(--fs-text-size-2)` |
| `--fs-search-product-item-title-line-height` | `1.2` |
| `--fs-search-product-item-title-margin-bottom` | `var(--fs-spacing-0)` |
| `--fs-search-product-item-price-size` | `var(--fs-text-size-base)` |
| `--fs-search-product-item-bkg-color-hover` | `var(--fs-color-tertiary-bkg-hover)` |
| `--fs-search-product-item-control-input-width` | `4.625rem` |
| `--fs-search-product-item-control-actions-gap` | `var(--fs-spacing-1)` |
**SearchTop**
| Variable | Default Value |
|---|---|
| `--fs-search-top-padding-top` | `var(--fs-spacing-2)` |
| `--fs-search-top-padding-bottom` | `var(--fs-search-top-padding-top)` |
| `--fs-search-top-padding-right` | `var(--fs-spacing-3)` |
| `--fs-search-top-padding-left` | `var(--fs-search-top-padding-right)` |
| `--fs-search-top-title-size` | `var(--fs-text-size-lead)` |
| `--fs-search-top-title-line-height` | `1.5` |
| `--fs-search-top-title-padding-top` | `var(--fs-spacing-1)` |
| `--fs-search-top-title-padding-bottom` | `var(--fs-search-top-title-padding-top)` |
| `--fs-search-top-item-text-size` | `var(--fs-text-size-2)` |
| `--fs-search-top-item-line-height` | `1.25` |
| `--fs-search-top-item-column-gap` | `var(--fs-spacing-1)` |
| `--fs-search-top-item-bkg-color-hover` | `var(--fs-color-tertiary-bkg-hover)` |
| `--fs-search-top-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-search-top-transition-property` | `var(--fs-transition-property)` |
| `--fs-search-top-transition-function` | `var(--fs-transition-function)` |
---
### SkuSelector
| Variable | Default Value |
|---|---|
| `--fs-sku-selector-text-size` | `var(--fs-text-size-1)` |
| `--fs-sku-selector-row-gap` | `var(--fs-spacing-2)` |
| `--fs-sku-selector-column-gap` | `var(--fs-sku-selector-row-gap)` |
| `--fs-sku-selector-image-width` | `var(--fs-spacing-6)` |
| `--fs-sku-selector-image-height` | `var(--fs-sku-selector-image-width)` |
| `--fs-sku-selector-image-border-radius` | `var(--fs-border-radius-small)` |
| `--fs-sku-selector-color-width` | `var(--fs-sku-selector-image-width)` |
| `--fs-sku-selector-color-height` | `var(--fs-sku-selector-color-width)` |
| `--fs-sku-selector-color-border-radius` | `var(--fs-sku-selector-image-border-radius)` |
| **Option** | |
| `--fs-sku-selector-option-width` | `var(--fs-spacing-7)` |
| `--fs-sku-selector-option-height` | `var(--fs-sku-selector-option-width)` |
| `--fs-sku-selector-option-border-radius` | `var(--fs-border-radius)` |
| `--fs-sku-selector-option-border-color` | `var(--fs-color-neutral-7)` |
| `--fs-sku-selector-option-border-color-hover` | `var(--fs-border-color-active)` |
| `--fs-sku-selector-option-border-width` | `var(--fs-border-width-thick)` |
| `--fs-sku-selector-option-bkg-color-hover` | `var(--fs-color-primary-bkg-light)` |
| `--fs-sku-selector-option-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-sku-selector-option-transition-function` | `ease` |
| **Checked** | |
| `--fs-sku-selector-option-checked-bkg-color` | `var(--fs-sku-selector-option-bkg-color-hover)` |
| `--fs-sku-selector-option-checked-border-color` | `var(--fs-sku-selector-option-border-color-hover)` |
| `--fs-sku-selector-option-checked-border-width` | `var(--fs-sku-selector-option-border-width)` |
| `--fs-sku-selector-option-checked-box-shadow` | `0 0 0 var(--fs-border-width-thickest) var(--fs-color-focus-ring-outline)` |
| **Disabled** | |
| `--fs-sku-selector-option-disabled-bkg-color` | `var(--fs-sku-selector-option-disabled-border-color)` |
| `--fs-sku-selector-option-disabled-border-color` | `var(--fs-border-color-disabled)` |
| `--fs-sku-selector-option-disabled-color` | `var(--fs-color-disabled-text)` |
| `--fs-sku-selector-option-disabled-width` | `var(--fs-border-width)` |
---
### Table
| Variable | Default Value |
|---|---|
| `--fs-table-cell-padding-x` | `var(--fs-spacing-3)` |
| `--fs-table-cell-padding-y` | `var(--fs-spacing-1)` |
| `--fs-table-head-bkg-color` | `none` |
| `--fs-table-head-weight` | `var(--fs-text-weight-bold)` |
| `--fs-table-head-padding-y` | `var(--fs-spacing-2)` |
| `--fs-table-footer-bkg-color` | `none` |
| `--fs-table-footer-weight` | `var(--fs-table-head-weight)` |
| `--fs-table-colored-bkg-color` | `var(--fs-color-neutral-1)` |
| `--fs-table-colored-border-radius` | `var(--fs-border-radius)` |
| `--fs-table-bordered-border-color` | `var(--fs-border-color-light)` |
| `--fs-table-bordered-border-width` | `var(--fs-border-width)` |
---
### Tag
| Variable | Default Value |
|---|---|
| `--fs-tag-text-color` | `var(--fs-color-text)` |
| `--fs-tag-icon-size` | `var(--fs-spacing-4)` |
| `--fs-tag-icon-stroke-width` | `var(--fs-spacing-4)` |
---
### TextareaField
| Variable | Default Value |
|---|---|
| `--fs-textarea-field-color` | `var(--fs-color-text)` |
| `--fs-textarea-field-size` | `var(--fs-text-size-body)` |
| `--fs-textarea-field-padding` | `22px var(--fs-spacing-2) 0` |
| `--fs-textarea-field-border-color` | `var(--fs-border-color)` |
| `--fs-textarea-field-button-height` | `var(--fs-control-tap-size)` |
| `--fs-textarea-field-label-color` | `var(--fs-color-text-light)` |
| `--fs-textarea-field-label-size` | `var(--fs-text-size-tiny)` |
| `--fs-textarea-field-label-padding` | `0 var(--fs-spacing-2) var(--fs-spacing-0) 0` |
| `--fs-textarea-field-label-left` | `var(--fs-spacing-2)` |
| `--fs-textarea-field-label-max-width` | `var(--fs-textarea-width)` |
| `--fs-textarea-field-label-placeholder-top-padding` | `var(--fs-spacing-2)` |
| `--fs-textarea-field-label-max-height` | `calc(var(--fs-textarea-height) - var(--fs-textarea-field-label-placeholder-top-padding))` |
| `--fs-textarea-field-label-background-color` | `var(--fs-color-neutral-0)` |
| `--fs-textarea-field-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-textarea-field-transition-property` | `var(--fs-transition-property)` |
| `--fs-textarea-field-transition-function` | `var(--fs-transition-function)` |
| **Disabled** | |
| `--fs-textarea-field-disabled-bkg-color` | `var(--fs-color-disabled-bkg)` |
| `--fs-textarea-field-disabled-border-color` | `var(--fs-border-color)` |
| `--fs-textarea-field-disabled-border-width` | `var(--fs-border-width)` |
| `--fs-textarea-field-disabled-text-color` | `var(--fs-color-disabled-text)` |
| **Error** | |
| `--fs-textarea-field-error-border-color` | `var(--fs-color-danger-border)` |
| `--fs-textarea-field-error-focus-ring` | `var(--fs-color-focus-ring-danger)` |
| `--fs-textarea-field-error-message-color` | `var(--fs-color-danger-text)` |
| `--fs-textarea-field-error-message-size` | `var(--fs-text-size-legend)` |
| `--fs-textarea-field-error-message-line-height` | `1.1` |
| `--fs-textarea-field-error-message-margin-top` | `var(--fs-spacing-0)` |
---
### Toast
| Variable | Default Value |
|---|---|
| `--fs-toast-width` | `calc(100% - (2 * var(--fs-spacing-3)))` |
| `--fs-toast-min-height` | `var(--fs-spacing-9)` |
| `--fs-toast-margin` | `var(--fs-spacing-3) var(--fs-spacing-3) 0 var(--fs-spacing-3)` |
| `--fs-toast-padding` | `var(--fs-spacing-1) var(--fs-spacing-3) var(--fs-spacing-1) var(--fs-spacing-1)` |
| `--fs-toast-bkg-color` | `var(--fs-color-neutral-0)` |
| `--fs-toast-border-radius` | `var(--fs-border-radius-medium)` |
| `--fs-toast-border-color` | `transparent` |
| `--fs-toast-border-width` | `var(--fs-border-width)` |
| `--fs-toast-shadow` | `0 1px 3px rgb(0 0 0 / 10%)` |
| `--fs-toast-top-mobile` | `3.125rem` |
| `--fs-toast-top-tablet` | `6.25rem` |
| `--fs-toast-title-size` | `var(--fs-text-size-body)` |
| `--fs-toast-title-weight` | `var(--fs-text-weight-bold)` |
| `--fs-toast-title-line-height` | `1.2` |
| `--fs-toast-title-margin-left` | `var(--fs-spacing-3)` |
| `--fs-toast-message-size` | `var(--fs-toast-title-size)` |
| `--fs-toast-message-line-height` | `var(--fs-toast-title-line-height)` |
| `--fs-toast-message-margin-left` | `var(--fs-spacing-3)` |
| `--fs-toast-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-toast-transition-property` | `var(--fs-transition-property)` |
| `--fs-toast-transition-function` | `var(--fs-transition-function)` |
| **Icon container** | |
| `--fs-toast-icon-container-min-width` | `var(--fs-spacing-7)` |
| `--fs-toast-icon-container-height` | `var(--fs-toast-icon-container-min-width)` |
| `--fs-toast-icon-container-bkg-color` | `var(--fs-color-primary-bkg-light)` |
| `--fs-toast-icon-container-border-radius` | `var(--fs-border-radius)` |
---
### Toggle
| Variable | Default Value |
|---|---|
| `--fs-toggle-height` | `calc(var(--fs-control-min-height) / 1.75)` |
| `--fs-toggle-border-radius` | `var(--fs-border-radius)` |
| `--fs-toggle-border-color` | `var(--fs-border-color)` |
| `--fs-toggle-border-color-hover` | `var(--fs-border-color-hover)` |
| `--fs-toggle-border-width` | `var(--fs-border-width)` |
| `--fs-toggle-bkg-color` | `var(--fs-control-bkg)` |
| `--fs-toggle-bkg-color-hover` | `var(--fs-color-primary-bkg-light)` |
| `--fs-toggle-shadow` | `var(--fs-shadow)` |
| `--fs-toggle-shadow-hover` | `var(--fs-shadow)` |
| `--fs-toggle-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-toggle-transition-property` | `var(--fs-transition-property)` |
| `--fs-toggle-transition-function` | `var(--fs-transition-function)` |
| **Checked** | |
| `--fs-toggle-checked-bkg-color` | `var(--fs-color-primary-bkg-active)` |
| `--fs-toggle-checked-bkg-color-hover` | `var(--fs-color-primary-bkg-hover)` |
| `--fs-toggle-checked-border-color` | `var(--fs-toggle-checked-bkg-color)` |
| `--fs-toggle-checked-border-color-hover` | `var(--fs-toggle-checked-bkg-color-hover)` |
| **Disabled** | |
| `--fs-toggle-disabled-bkg-color` | `var(--fs-color-disabled-bkg)` |
| `--fs-toggle-disabled-border-color` | `var(--fs-border-color-disabled)` |
| **Knob** | |
| `--fs-toggle-knob-bkg-color` | `var(--fs-color-primary-bkg)` |
| `--fs-toggle-knob-bkg-color-hover` | `var(--fs-toggle-border-color-hover)` |
| `--fs-toggle-knob-border-color` | `var(--fs-toggle-knob-bkg-color)` |
| `--fs-toggle-knob-border-color-hover` | `var(--fs-toggle-knob-bkg-color-hover)` |
| `--fs-toggle-knob-border-radius` | `var(--fs-border-radius-small)` |
| `--fs-toggle-knob-border-width` | `var(--fs-border-width-thick)` |
| `--fs-toggle-knob-shadow` | `var(--fs-shadow)` |
| `--fs-toggle-knob-icon-color` | `transparent` |
| `--fs-toggle-knob-icon-checked-color` | `var(--fs-toggle-checked-bkg-color)` |
| `--fs-toggle-knob-icon-checked-color-hover` | `var(--fs-toggle-checked-bkg-color-hover)` |
| `--fs-toggle-knob-checked-bkg-color` | `var(--fs-control-bkg)` |
| `--fs-toggle-knob-checked-border-color` | `var(--fs-toggle-knob-checked-bkg-color)` |
| `--fs-toggle-knob-disabled-bkg-color` | `var(--fs-color-neutral-5)` |
| `--fs-toggle-knob-disabled-border-color` | `var(--fs-toggle-knob-disabled-bkg-color)` |
| `--fs-toggle-knob-icon-disabled-color` | `var(--fs-toggle-disabled-bkg-color)` |
---
### Tooltip
| Variable | Default Value |
|---|---|
| `--fs-tooltip-padding` | `var(--fs-spacing-2)` |
| `--fs-tooltip-gap` | `var(--fs-spacing-1)` |
| `--fs-tooltip-border-radius` | `var(--fs-border-radius)` |
| `--fs-tooltip-background` | `var(--fs-color-neutral-6)` |
| `--fs-tooltip-text-color` | `var(--fs-color-text-inverse)` |
| `--fs-tooltip-z-index` | `var(--fs-z-index-high)` |
| `--fs-tooltip-indicator-size` | `var(--fs-spacing-1)` |
| `--fs-tooltip-indicator-distance-base` | `var(--fs-spacing-1)` |
| `--fs-tooltip-indicator-distance-edge` | `var(--fs-spacing-3)` |
| `--fs-tooltip-indicator-translate` | `calc(--fs-tooltip-indicator-size + --fs-tooltip-indicator-distance-base)` |
| `--fs-tooltip-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-tooltip-transition-property` | `opacity` |
| `--fs-tooltip-transition-function` | `var(--fs-transition-function)` |
---
## Organisms
### BannerText
| Variable | Default Value |
|---|---|
| `--fs-banner-text-padding-mobile` | `var(--fs-spacing-6) 5%` |
| `--fs-banner-text-padding-desktop` | `var(--fs-spacing-9) 10%` |
| `--fs-banner-text-border-radius` | `var(--fs-border-radius)` |
| `--fs-banner-text-title-size` | `var(--fs-text-size-lead)` |
| `--fs-banner-text-title-weight` | `var(--fs-text-weight-bold)` |
| `--fs-banner-text-title-line-height` | `1.2` |
| `--fs-banner-text-primary-title-size` | `var(--fs-text-size-title-page)` |
| `--fs-banner-text-secondary-title-size` | `var(--fs-text-size-4)` |
| `--fs-banner-text-secondary-caption-size` | `var(--fs-text-size-base)` |
| `--fs-banner-text-secondary-caption-weight` | `var(--fs-text-weight-regular)` |
| `--fs-banner-text-secondary-caption-line-height` | `1.5` |
| `--fs-banner-text-button-link-min-width` | `11.25rem` |
| `--fs-banner-text-button-link-margin-top` | `var(--fs-spacing-6)` |
| **Variants** | |
| `--fs-banner-text-main-bkg-color` | `var(--fs-color-primary-bkg)` |
| `--fs-banner-text-main-text-color` | `var(--fs-color-primary-text)` |
| `--fs-banner-text-light-bkg-color` | `var(--fs-color-secondary-bkg-light)` |
| `--fs-banner-text-light-text-color` | `var(--fs-color-text-display)` |
| `--fs-banner-text-accent-bkg-color` | `var(--fs-color-highlighted-bkg)` |
| `--fs-banner-text-accent-text-color` | `var(--fs-banner-text-light-text-color)` |
---
### CartSidebar
| Variable | Default Value |
|---|---|
| `--fs-cart-sidebar-bkg-color` | `var(--fs-color-neutral-bkg)` |
| `--fs-cart-sidebar-list-padding` | `var(--fs-spacing-3)` |
| `--fs-cart-sidebar-header-title-column-gap` | `var(--fs-spacing-2)` |
| `--fs-cart-sidebar-footer-bkg-color` | `var(--fs-color-neutral-0)` |
| `--fs-cart-sidebar-footer-box-shadow` | `0 0 6px rgb(0 0 0 / 20%)` |
---
### EmptyState
| Variable | Default Value |
|---|---|
| `--fs-empty-state-height` | `100%` |
| `--fs-empty-state-min-height` | `50vh` |
| `--fs-empty-state-border-radius` | `var(--fs-border-radius)` |
| `--fs-empty-state-padding` | `0 var(--fs-spacing-8)` |
| `--fs-empty-state-bkg-color-default` | `var(--fs-color-neutral-bkg)` |
| `--fs-empty-state-bkg-color-light` | `var(--fs-color-body-bkg)` |
| `--fs-empty-state-title-color` | `var(--fs-color-disabled-text)` |
| `--fs-empty-state-title-size` | `var(--fs-text-size-lead)` |
| `--fs-empty-state-title-margin-bottom` | `var(--fs-spacing-2)` |
| `--fs-empty-state-link-min-width` | `11.875rem` |
---
### Filter
| Variable | Default Value |
|---|---|
| `--fs-filter-title-height` | `var(--fs-spacing-6)` |
| `--fs-filter-title-margin-bottom` | `var(--fs-spacing-0)` |
| `--fs-filter-title-text-size` | `var(--fs-text-size-2)` |
| `--fs-filter-title-line-height` | `1.25` |
| `--fs-filter-link-color` | `var(--fs-color-link)` |
| `--fs-filter-link-column-gap` | `var(--fs-spacing-0)` |
| `--fs-filter-link-padding` | `0` |
| `--fs-filter-list-padding-bottom` | `var(--fs-spacing-3)` |
| `--fs-filter-list-item-not-last-margin-bottom` | `var(--fs-spacing-3)` |
| `--fs-filter-list-item-checkbox-width` | `1.25rem` |
| `--fs-filter-list-item-checkbox-height` | `var(--fs-filter-list-item-checkbox-width)` |
| `--fs-filter-list-item-label-text-size` | `var(--fs-text-size-2)` |
| `--fs-filter-list-item-label-line-height` | `1.25` |
| `--fs-filter-list-item-label-width` | `100%` |
| `--fs-filter-list-item-label-margin-left` | `var(--fs-spacing-1)` |
| `--fs-filter-list-item-badge-margin-left` | `var(--fs-spacing-1)` |
| **Accordion (notebook)** | |
| `--fs-filter-accordion-border-width-notebook` | `var(--fs-border-width)` |
| `--fs-filter-accordion-border-color-notebook` | `var(--fs-border-color-light)` |
| `--fs-filter-accordion-border-radius-notebook` | `var(--fs-border-radius)` |
| `--fs-filter-accordion-button-text-size` | `var(--fs-text-size-lead)` |
| `--fs-filter-accordion-button-text-size-notebook` | `var(--fs-text-size-2)` |
| `--fs-filter-accordion-button-text-weight` | `var(--fs-text-weight-regular)` |
| `--fs-filter-accordion-button-line-height` | `1.5` |
| `--fs-filter-accordion-button-line-height-notebook` | `1.25` |
| `--fs-filter-accordion-button-padding-right-notebook` | `var(--fs-spacing-4)` |
| `--fs-filter-accordion-button-padding-left-notebook` | `var(--fs-filter-accordion-button-padding-right-notebook)` |
| `--fs-filter-accordion-item-panel-padding-right-notebook` | `var(--fs-spacing-4)` |
| `--fs-filter-accordion-item-panel-padding-left-notebook` | `var(--fs-filter-accordion-item-panel-padding-right-notebook)` |
---
### FilterSkeleton
| Variable | Default Value |
|---|---|
| `--fs-filter-skeleton-margin-top` | `var(--fs-spacing-1)` |
| `--fs-filter-skeleton-title-max-width` | `30%` |
| `--fs-filter-skeleton-title-margin-bottom` | `var(--fs-spacing-2)` |
| `--fs-filter-skeleton-content-min-height` | `var(--fs-spacing-8)` |
| `--fs-filter-skeleton-content-margin-bottom` | `var(--fs-spacing-0)` |
| `--fs-filter-skeleton-content-padding` | `var(--fs-spacing-1) var(--fs-spacing-1) var(--fs-spacing-0)` |
| `--fs-filter-skeleton-content-border-color` | `var(--fs-border-color-light)` |
| `--fs-filter-skeleton-content-border-width` | `var(--fs-border-width)` |
| `--fs-filter-skeleton-content-border-radius` | `var(--fs-border-radius)` |
---
### FilterSlider
| Variable | Default Value |
|---|---|
| `--fs-filter-slider-footer-height` | `5rem` |
| `--fs-filter-slider-footer-width` | `100%` |
| `--fs-filter-slider-footer-padding` | `var(--fs-spacing-3)` |
| `--fs-filter-slider-footer-bkg-color` | `var(--fs-color-neutral-0)` |
| `--fs-filter-slider-footer-box-shadow` | `0 0 6px rgb(0 0 0 / 20%)` |
| `--fs-filter-slider-footer-button-clear-width` | `40%` |
| `--fs-filter-slider-footer-button-clear-margin-right` | `var(--fs-spacing-3)` |
| `--fs-filter-slider-footer-button-apply-width` | `60%` |
| `--fs-filter-slider-content-height` | `calc(100vh - var(--fs-filter-slider-footer-height))` |
| `--fs-filter-slider-content-padding` | `0 var(--fs-spacing-3)` |
| `--fs-filter-slider-title-font-size` | `var(--fs-text-size-3)` |
| `--fs-filter-slider-title-font-weight` | `var(--fs-text-weight-semibold)` |
| `--fs-filter-slider-title-line-height` | `1.12` |
---
### Footer
| Variable | Default Value |
|---|---|
| `--fs-footer-spacing-vertical-mobile` | `var(--fs-spacing-4)` |
| `--fs-footer-spacing-vertical-notebook` | `var(--fs-spacing-5)` |
| `--fs-footer-spacing-horizontal-notebook` | `var(--fs-grid-gap-3)` |
| `--fs-footer-bkg-color` | `var(--fs-color-neutral-bkg)` |
| `--fs-footer-divisor-border-width` | `var(--fs-border-width)` |
| `--fs-footer-divisor-border-color` | `var(--fs-border-color-light)` |
| `--fs-footer-title-size` | `var(--fs-text-size-body)` |
| `--fs-footer-title-line-height` | `1.25` |
| `--fs-footer-title-weight` | `var(--fs-text-weight-bold)` |
| `--fs-footer-title-margin-bottom` | `var(--fs-spacing-1)` |
| `--fs-footer-logo-width` | `var(--fs-logo-width)` |
---
### Hero
| Variable | Default Value |
|---|---|
| `--fs-hero-text-size` | `var(--fs-text-size-lead)` |
| `--fs-hero-text-line-height` | `1.33` |
| `--fs-hero-image-border-radius` | `0` |
| `--fs-hero-title-padding` | `var(--fs-spacing-5) 0 var(--fs-spacing-6)` |
| `--fs-hero-title-weight` | `var(--fs-text-weight-black)` |
| `--fs-hero-title-line-height` | `1.1` |
| `--fs-hero-subtitle-margin-top-mobile` | `var(--fs-spacing-2)` |
| `--fs-hero-subtitle-margin-top-tablet` | `var(--fs-spacing-4)` |
| `--fs-hero-subtitle-size` | `var(--fs-hero-text-size)` |
| `--fs-hero-subtitle-line-height` | `var(--fs-hero-text-line-height)` |
| `--fs-hero-primary-title-size` | `var(--fs-text-size-title-huge)` |
| `--fs-hero-primary-image-height-mobile` | `15rem` |
| `--fs-hero-primary-image-height-desktop` | `29rem` |
| `--fs-hero-secondary-title-size` | `var(--fs-text-size-title-page)` |
| `--fs-hero-secondary-image-height-mobile` | `11.25rem` |
| `--fs-hero-secondary-image-height-desktop` | `14.188rem` |
| **Variants** | |
| `--fs-hero-main-bkg-color` | `var(--fs-color-primary-bkg)` |
| `--fs-hero-main-text-color` | `var(--fs-color-primary-text)` |
| `--fs-hero-light-bkg-color` | `var(--fs-color-secondary-bkg-light)` |
| `--fs-hero-light-text-color` | `var(--fs-color-text-display)` |
| `--fs-hero-accent-bkg-color` | `var(--fs-color-highlighted-bkg)` |
| `--fs-hero-accent-text-color` | `var(--fs-hero-light-text-color)` |
---
### ImageGallery
| Variable | Default Value |
|---|---|
| `--fs-image-gallery-width` | `calc(100% + (2 * var(--fs-grid-padding)))` |
| `--fs-image-gallery-gap-mobile` | `var(--fs-spacing-2)` |
| `--fs-image-gallery-gap-notebook` | `var(--fs-spacing-3)` |
| `--fs-image-gallery-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-image-gallery-transition-function` | `var(--fs-transition-function)` |
| **Current image** | |
| `--fs-image-gallery-current-height` | `33.125rem` |
| `--fs-image-gallery-current-border-radius` | `var(--fs-border-radius)` |
| **Selector** | |
| `--fs-image-gallery-selector-max-height` | `var(--fs-image-gallery-current-height)` |
| `--fs-image-gallery-selector-elements-gap` | `var(--fs-spacing-1)` |
| `--fs-image-gallery-selector-elements-gap-notebook` | `var(--fs-spacing-2)` |
| `--fs-image-gallery-selector-elements-padding-mobile` | `var(--fs-spacing-0) var(--fs-grid-padding)` |
| `--fs-image-gallery-selecssctor-elements-padding-notebook` | `var(--fs-spacing-0) 0` |
| `--fs-image-gallery-selector-control-bkg-color` | `var(--fs-control-bkg)` |
| `--fs-image-gallery-selector-control-border-radius` | `var(--fs-border-radius-circle)` |
| `--fs-image-gallery-selector-control-shadow` | `var(--fs-shadow-darker)` |
| `--fs-image-gallery-selector-control-gradient-bkg-color` | `var(--fs-color-body-bkg)` |
| **Thumbnails** | |
| `--fs-image-gallery-selector-thumbnail-width-mobile` | `var(--fs-spacing-8)` |
| `--fs-image-gallery-selector-thumbnail-height-mobile` | `var(--fs-image-gallery-selector-thumbnail-width-mobile)` |
| `--fs-image-gallery-selector-thumbnail-width-notebook` | `var(--fs-spacing-10)` |
| `--fs-image-gallery-selector-thumbnail-height-notebook` | `var(--fs-image-gallery-selector-thumbnail-width-notebook)` |
| `--fs-image-gallery-selector-thumbnail-border-radius` | `var(--fs-border-radius)` |
| `--fs-image-gallery-selector-thumbnail-border-width` | `var(--fs-border-width-thick)` |
| `--fs-image-gallery-selector-thumbnail-image-border-radius` | `var(--fs-border-radius-small)` |
| `--fs-image-gallery-selector-thumbnail-selected-border-color` | `var(--fs-border-color-active)` |
| `--fs-image-gallery-selector-thumbnail-selected-border-width` | `var(--fs-border-width-thickest)` |
---
### Incentives
| Variable | Default Value |
|---|---|
| `--fs-incentives-gap` | `var(--fs-spacing-4)` |
| `--fs-incentives-padding-top` | `var(--fs-incentives-gap)` |
| `--fs-incentives-padding-bottom` | `var(--fs-incentives-gap)` |
| `--fs-incentives-bkg-color` | `var(--fs-color-primary-bkg-light)` |
| `--fs-incentives-border-color` | `var(--fs-border-color-light)` |
| `--fs-incentives-border-width` | `var(--fs-border-width)` |
| `--fs-incentives-title-size` | `var(--fs-text-size-1)` |
| `--fs-incentives-title-weight` | `var(--fs-text-weight-bold)` |
| `--fs-incentives-title-line-height` | `1.42` |
| `--fs-incentives-title-color` | `var(--fs-color-text)` |
| `--fs-incentives-description-size` | `var(--fs-incentives-title-size)` |
| `--fs-incentives-description-line-height` | `1.14` |
| `--fs-incentives-description-color` | `var(--fs-incentives-title-color)` |
| `--fs-incentives-icon-color` | `var(--fs-incentives-title-color)` |
---
### Navbar
| Variable | Default Value |
|---|---|
| `--fs-navbar-height-mobile` | `3.5rem` |
| `--fs-navbar-bkg-color` | `rgb(255 255 255 / 90%)` |
| `--fs-navbar-box-shadow` | `0 var(--fs-spacing-0) var(--fs-spacing-3) rgb(0 0 0 / 5%)` |
| `--fs-navbar-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-navbar-transition-function` | `var(--fs-transition-function)` |
| `--fs-navbar-header-padding` | `0 var(--fs-spacing-0)` |
| `--fs-navbar-header-padding-top-notebook` | `var(--fs-spacing-1)` |
| `--fs-navbar-header-padding-bottom-notebook` | `var(--fs-navbar-header-padding-top-notebook)` |
| **Search** | |
| `--fs-navbar-search-button-icon-width-mobile` | `var(--fs-spacing-5)` |
| `--fs-navbar-search-button-icon-height-mobile` | `var(--fs-navbar-search-button-icon-width-mobile)` |
| `--fs-navbar-search-expanded-input-width` | `calc(100% - var(--fs-spacing-7))` |
| `--fs-navbar-search-expanded-button-icon-margin-right` | `-4.063rem` |
| **Logo** | |
| `--fs-navbar-logo-width` | `var(--fs-logo-width)` |
| `--fs-navbar-logo-border-left-width` | `var(--fs-border-width)` |
| `--fs-navbar-logo-border-left-color` | `var(--fs-border-color-light)` |
---
### NavbarSlider
| Variable | Default Value |
|---|---|
| `--fs-navbar-slider-padding` | `var(--fs-spacing-3)` |
| `--fs-navbar-slider-header-height` | `5rem` |
| `--fs-navbar-slider-header-padding-bottom` | `var(--fs-spacing-2)` |
| `--fs-navbar-slider-header-button-margin-right` | `calc(-1 * var(--fs-spacing-1))` |
| `--fs-navbar-slider-footer-padding-top` | `var(--fs-navbar-slider-header-padding-bottom)` |
| `--fs-navbar-slider-footer-margin-top` | `var(--fs-navbar-slider-header-padding-bottom)` |
| `--fs-navbar-slider-logo-padding` | `0` |
| `--fs-navbar-slider-logo-margin-right` | `var(--fs-spacing-5)` |
---
### Newsletter
| Variable | Default Value |
|---|---|
| `--fs-newsletter-padding-mobile` | `var(--fs-spacing-5)` |
| `--fs-newsletter-padding-desktop` | `var(--fs-spacing-9) 10%` |
| `--fs-newsletter-border-radius` | `var(--fs-border-radius)` |
| `--fs-newsletter-card-border-radius` | `var(--fs-border-radius)` |
| `--fs-newsletter-icon-size` | `var(--fs-spacing-5)` |
| `--fs-newsletter-title-size` | `var(--fs-text-size-title-section)` |
| `--fs-newsletter-title-weight` | `var(--fs-text-weight-bold)` |
| **Variants** | |
| `--fs-newsletter-main-bkg-color` | `var(--fs-color-primary-bkg)` |
| `--fs-newsletter-main-text-color` | `var(--fs-color-primary-text)` |
| `--fs-newsletter-light-bkg-color` | `var(--fs-color-secondary-bkg-light)` |
| `--fs-newsletter-light-text-color` | `var(--fs-color-text-display)` |
| `--fs-newsletter-accent-bkg-color` | `var(--fs-color-highlighted-bkg)` |
| `--fs-newsletter-accent-text-color` | `var(--fs-newsletter-light-text-color)` |
---
### OutOfStock
| Variable | Default Value |
|---|---|
| `--fs-out-of-stock-title-size` | `var(--fs-text-size-lead)` |
| `--fs-out-of-stock-title-weight` | `var(--fs-text-weight-bold)` |
| `--fs-out-of-stock-title-line-height` | `1.15` |
| `--fs-out-of-stock-title-margin-bottom` | `var(--fs-spacing-0)` |
| `--fs-out-of-stock-title-color` | `var(--fs-color-neutral-text)` |
| `--fs-out-of-stock-message-size` | `var(--fs-text-size-body)` |
| `--fs-out-of-stock-message-weight` | `var(--fs-text-weight-regular)` |
| `--fs-out-of-stock-message-line-height` | `1.15` |
| `--fs-out-of-stock-message-color` | `var(--fs-color-success-text)` |
| `--fs-out-of-stock-message-column-gap` | `var(--fs-spacing-0)` |
| `--fs-out-of-stock-message-margin-bottom` | `var(--fs-spacing-3)` |
| `--fs-out-of-stock-button-width` | `100%` |
| `--fs-out-of-stock-button-margin-top` | `var(--fs-spacing-3)` |
---
### PaymentMethods
| Variable | Default Value |
|---|---|
| `--fs-payment-methods-title-size` | `var(--fs-text-size-body)` |
| `--fs-payment-methods-title-weight` | `var(--fs-text-weight-bold)` |
| `--fs-payment-methods-title-line-height` | `1.25` |
| `--fs-payment-methods-flag-width` | `var(--fs-spacing-5)` |
| `--fs-payment-methods-flag-height` | `var(--fs-spacing-4)` |
| `--fs-payment-methods-flag-bkg-color` | `var(--fs-color-neutral-0)` |
| `--fs-payment-methods-flag-border-width` | `var(--fs-border-width)` |
| `--fs-payment-methods-flag-border-color` | `var(--fs-color-neutral-3)` |
| `--fs-payment-methods-flag-border-radius` | `var(--fs-border-radius-small)` |
| `--fs-payment-methods-flags-row-gap` | `var(--fs-spacing-1)` |
| `--fs-payment-methods-flags-margin-top` | `var(--fs-spacing-3)` |
---
### PickupPointCards
| Variable | Default Value |
|---|---|
| `--fs-pickup-point-cards-row-gap` | `var(--fs-grid-gap-2)` |
| `--fs-pickup-point-cards-item-bkg-color-hover` | `var(--fs-color-neutral-bkg)` |
| `--fs-pickup-point-cards-item-border-color-selected` | `var(--fs-border-color-active)` |
| `--fs-pickup-point-cards-item-border-width-selected` | `var(--fs-border-width-thick)` |
---
### ProductComparison
| Variable | Default Value |
|---|---|
| `--fs-product-comparison-bkg-color` | `var(--fs-color-body-bkg)` |
| `--fs-product-comparison-bkg-color-neutral` | `var(--fs-color-neutral-1)` |
| `--fs-product-comparison-box-shadow` | `var(--fs-shadow-darker)` |
| `--fs-product-comparison-padding` | `var(--fs-spacing-8)` |
| `--fs-product-comparison-text-weight` | `var(--fs-text-weight-light)` |
| `--fs-product-comparison-text-color` | `var(--fs-border-color-light)` |
| `--fs-product-comparison-title-size` | `var(--fs-text-size-6)` |
| `--fs-product-comparison-title-weight` | `var(--fs-text-weight-semibold)` |
| `--fs-product-comparison-slide-over-partial-gap` | `calc(2 * var(--fs-grid-padding))` |
| `--fs-product-comparison-slide-over-partial-width-mobile` | `calc(100vw - var(--fs-slide-over-partial-gap))` |
| `--fs-product-comparison-slide-over-partial-width-notebook` | `calc(100% / 3)` |
| `--fs-product-comparison-slide-over-partial-max-width-notebook` | `calc(var(--fs-grid-breakpoint-notebook) / 3)` |
---
### ProductDetails
| Variable | Default Value |
|---|---|
| `--fs-product-details-vertical-spacing` | `var(--fs-spacing-4)` |
| `--fs-product-details-horizontal-spacing` | `var(--fs-product-details-vertical-spacing)` |
| `--fs-product-details-section-bkg-color` | `transparent` |
| `--fs-product-details-section-border-radius` | `var(--fs-border-radius)` |
| `--fs-product-details-section-border-color` | `var(--fs-border-color-light)` |
| `--fs-product-details-section-border-width` | `var(--fs-border-width)` |
---
### ProductGrid
| Variable | Default Value |
|---|---|
| `--fs-product-grid-gap-mobile` | `var(--fs-grid-gap-0)` |
| `--fs-product-grid-gap-tablet` | `var(--fs-product-grid-gap-mobile)` |
| `--fs-product-grid-gap-desktop` | `var(--fs-grid-gap-2)` |
| `--fs-product-grid-columns-mobile` | `2` |
| `--fs-product-grid-columns-tablet` | `4` |
| `--fs-product-grid-columns-desktop` | `var(--fs-product-grid-columns-tablet)` |
---
### ProductShelf
| Variable | Default Value |
|---|---|
| `--fs-product-shelf-items-gap` | `var(--fs-grid-gap-1)` |
| `--fs-product-shelf-items-padding-top` | `var(--fs-spacing-0)` |
| `--fs-product-shelf-items-padding-bottom` | `var(--fs-spacing-3)` |
---
### RegionModal
| Variable | Default Value |
|---|---|
| `--fs-region-modal-margin-bottom` | `var(--fs-spacing-6)` |
| `--fs-region-modal-link-color` | `var(--fs-color-link)` |
| `--fs-region-modal-link-padding` | `0` |
| `--fs-region-modal-link-column-gap` | `var(--fs-spacing-0)` |
---
### RegionPopover
| Variable | Default Value |
|---|---|
| `--fs-region-popover-width` | `406px` |
| `--fs-region-popover-row-gap` | `var(--fs-spacing-2)` |
| `--fs-region-popover-description-text-size` | `var(--fs-text-size-legend)` |
| `--fs-region-popover-link-padding` | `0` |
| `--fs-region-popover-link-column-gap` | `var(--fs-spacing-0)` |
| `--fs-region-popover-link-color` | `var(--fs-color-link)` |
---
### SearchInput
| Variable | Default Value |
|---|---|
| `--fs-search-input-height-desktop` | `var(--fs-spacing-6)` |
---
### ShippingSimulation
| Variable | Default Value |
|---|---|
| `--fs-shipping-simulation-header-padding-top` | `var(--fs-spacing-3)` |
| `--fs-shipping-simulation-title-font-size` | `var(--fs-text-size-3)` |
| `--fs-shipping-simulation-title-font-weight` | `var(--fs-text-weight-bold)` |
| `--fs-shipping-simulation-title-line-height` | `1.2` |
| `--fs-shipping-simulation-title-padding-bottom` | `var(--fs-spacing-2)` |
| `--fs-shipping-simulation-subtitle-size` | `var(--fs-text-size-2)` |
| `--fs-shipping-simulation-subtitle-weight` | `var(--fs-text-weight-bold)` |
| `--fs-shipping-simulation-subtitle-line-height` | `1.5` |
| `--fs-shipping-simulation-text-size` | `var(--fs-text-size-legend)` |
| `--fs-shipping-simulation-location-font-size` | `var(--fs-text-size-2)` |
| `--fs-shipping-simulation-location-line-height` | `1.5` |
| `--fs-shipping-simulation-location-padding-bottom` | `var(--fs-spacing-2)` |
| `--fs-shipping-simulation-link-padding-top` | `var(--fs-spacing-1)` |
---
### SKUMatrix
| Variable | Default Value |
|---|---|
| `--fs-sku-matrix-sidebar-bkg-color` | `var(--fs-color-body-bkg)` |
| `--fs-sku-matrix-sidebar-title-size` | `var(--fs-text-size-6)` |
| `--fs-sku-matrix-sidebar-title-text-weight` | `var(--fs-text-weight-semibold)` |
| `--fs-sku-matrix-sidebar-table-cell-font-size` | `var(--fs-text-size-tiny)` |
| `--fs-sku-matrix-sidebar-table-cell-text-weight` | `var(--fs-text-weight-medium)` |
| `--fs-sku-matrix-sidebar-table-cell-image-width` | `var(--fs-spacing-7)` |
| `--fs-sku-matrix-sidebar-table-cell-image-border-radius` | `var(--fs-border-radius)` |
| `--fs-sku-matrix-slide-over-partial-gap` | `calc(2 * var(--fs-grid-padding))` |
| `--fs-sku-matrix-slide-over-partial-width-mobile` | `calc(100vw - var(--fs-sku-matrix-slide-over-partial-gap))` |
---
### SlideOver
| Variable | Default Value |
|---|---|
| `--fs-slide-over-bkg-color` | `var(--fs-color-body-bkg)` |
| `--fs-slide-over-transition-timing` | `var(--fs-transition-timing)` |
| `--fs-slide-over-header-padding` | `var(--fs-spacing-2) var(--fs-spacing-3) var(--fs-spacing-2)` |
| `--fs-slide-over-header-bkg-color` | `var(--fs-color-neutral-0)` |
| `--fs-slide-over-partial-gap` | `calc(2 * var(--fs-grid-padding))` |
| `--fs-slide-over-partial-width-mobile` | `calc(100vw - var(--fs-slide-over-partial-gap))` |
| `--fs-slide-over-partial-width-notebook` | `calc(100% / 3)` |
| `--fs-slide-over-partial-max-width-notebook` | `calc(var(--fs-grid-breakpoint-notebook) / 3)` |
---
### Tiles
| Variable | Default Value |
|---|---|
| `--fs-tiles-gap-mobile` | `var(--fs-grid-gap-2)` |
| `--fs-tiles-gap-notebook` | `var(--fs-grid-gap-3)` |
| `--fs-tiles-tile-min-width` | `9rem` |
| `--fs-tiles-tile-border-radius` | `var(--fs-border-radius)` |
references/search-facets-and-usesearch-api.md
---
name: faststore-search
description: FastStore search and facets reference covering common pitfalls, page context data flow for PLP/Search pages, facets access patterns, and useSearch Zustand API. Use when building custom sections that need search facets, toggling filters, or accessing search state on PLP and Search pages.
metadata:
author: vtex
version: "1.0"
---
# FastStore Search & Facets Reference
## Common Pitfalls
### Accessing facets in custom PLP/Search sections
- Facets are **NOT** available in the server-side page data (`__NEXT_DATA__`). They come from the **client-side** `useProductGalleryQuery` inside the `ProductListing` template, which deep-merges them into the `PageProvider` context.
- After client-side hydration, facets are accessible via `usePage()`:
```tsx
const context = usePage<PLPContext | SearchPageContext>();
const facets = (context as any)?.data?.search?.facets ?? [];
```
- The component will initially render with no facets (returns `null`), then re-render once the client-side query completes and the context updates.
### Facet type discrimination
- GraphQL facets use `__typename` for type discrimination, **NOT** a `type` field:
- `__typename === "StoreFacetBoolean"` → has `values[]` with `{ value, label, selected, quantity }`
- `__typename === "StoreFacetRange"` → price ranges, no `values` array
- **Do NOT filter with `f.type === "BOOLEAN"`** — this field does not exist on the runtime object.
### useSearch() API — Zustand store
The `useSearch()` hook from `@faststore/sdk` returns a **Zustand global store**, not the React Context documented in older references. Key differences:
| ✅ Correct (Zustand store) | ❌ Wrong (old Context API) |
| ----------------------------------------- | ----------------------------------------------- |
| `const { state, setState } = useSearch()` | `const { setFacet, removeFacet } = useSearch()` |
| `state.selectedFacets` | Direct method calls on the hook return |
To toggle a facet, import the **standalone utility** `toggleFacet` from `@faststore/sdk`:
```tsx
import { useSearch, toggleFacet } from "@faststore/sdk";
const { state, setState } = useSearch();
const newFacets = toggleFacet(state.selectedFacets, { key, value });
setState({ ...state, selectedFacets: newFacets, page: 0 });
```
Available utilities from `@faststore/sdk`:
- `toggleFacet(facets, facet)` → add if absent, remove if present
- `setFacet(facets, facet, unique?)` → add a facet
- `removeFacet(facets, facet)` → remove a facet
- `toggleFacets(facets, facets[])` → toggle multiple at once
## Page Context Data Flow (PLP/Search)
The PLP page context is built in two phases:
1. **Server-side** (`getStaticProps`): `ServerCollectionPageQuery` → returns `collection.seo`, `collection.breadcrumbList`, `collection.metaData`. **No facets.**
2. **Client-side** (`useProductGalleryQuery`): `ClientProductGalleryQuery` → returns `search.products`, `search.facets`, `search.metadata`.
The `ProductListing` template merges both via `deepmerge` into the `PageProvider` context:
```tsx
// From @faststore/core ProductListing.tsx
const { data: pageProductGalleryData } = useProductGalleryQuery({
term,
sort,
selectedFacets,
itemsPerPage,
});
const context = {
data: {
...deepmerge(
{ ...server },
{ ...pageProductGalleryData },
{ arrayMerge: overwriteMerge },
),
pages,
},
globalSettings,
} as PLPContext;
```
After client-side hydration: `usePage().data.search.facets` is available.
## PLPContext type
```ts
interface PLPContext {
data?: ServerCollectionPageQueryQuery & // server: collection, seo
ClientProductGalleryQueryQuery & { pages: ClientManyProductsQueryQuery[] }; // client: search.facets, search.products
globalSettings?: Record<string, unknown>;
}
```
## Accessing facets in a custom section
```tsx
import { usePage } from "@faststore/core";
import type { PLPContext, SearchPageContext } from "@faststore/core";
const context = usePage<PLPContext | SearchPageContext>();
const facets = (context as any)?.data?.search?.facets ?? [];
```
**Important:** On initial server render, facets will be empty. The component should handle this gracefully (e.g., return `null`). After client-side hydration and the ProductGalleryQuery completes, React will re-render the component with facets.
## Facet type discrimination
GraphQL facets use `__typename`, NOT a `type` field:
| `__typename` | Description | Has `values[]`? |
| ------------------- | ----------------------------------- | ------------------------------------------- |
| `StoreFacetBoolean` | Checkbox-style facets (brand, size) | Yes: `{ value, label, selected, quantity }` |
| `StoreFacetRange` | Range facets (price) | No |
```tsx
const booleanFacets = facets.filter(
(f) => f.__typename === "StoreFacetBoolean" && f.values?.length > 0,
);
```
## useSearch() — Zustand store API
The `useSearch()` hook from `@faststore/sdk` returns a Zustand global store:
```tsx
import { useSearch } from "@faststore/sdk";
const { state, setState } = useSearch();
// state.selectedFacets — { key: string, value: string }[]
// state.sort — StoreSort enum string
// state.term — search term or null
// state.page — current page index
// setState(partial) — merges partial state into current
```
**`setFacet` and `removeFacet` are NOT methods on the hook return object.** They are standalone utility functions.
## Facet toggle utilities
Imported as standalone functions from `@faststore/sdk`:
```tsx
import {
toggleFacet,
setFacet,
removeFacet,
toggleFacets,
} from "@faststore/sdk";
```
All are **pure functions**: `(currentFacets[], facet) => newFacets[]`
```tsx
import { useSearch, toggleFacet } from "@faststore/sdk";
const { state, setState } = useSearch();
function handleToggle(key: string, value: string) {
const newFacets = toggleFacet(state.selectedFacets, { key, value });
setState({ ...state, selectedFacets: newFacets, page: 0 });
}
```
| Function | Behavior |
| ---------------------------------- | -------------------------------------------- |
| `toggleFacet(facets, facet)` | Add if absent, remove if present |
| `setFacet(facets, facet, unique?)` | Add a facet (if `unique`, replaces same-key) |
| `removeFacet(facets, facet)` | Remove a facet by value |
| `toggleFacets(facets, facets[])` | Toggle multiple facets at once |
## @faststore/sdk — exports map
| Export | Type | Usage |
| -------------------- | -------------------- | ----------------------------------------------------- |
| `useSearch` | Hook (Zustand store) | `const { state, setState } = useSearch()` |
| `SearchProvider` | React Component | Wraps PLP/Search pages (already handled by framework) |
| `toggleFacet` | Pure function | `toggleFacet(facets[], facet) → newFacets[]` |
| `setFacet` | Pure function | `setFacet(facets[], facet, unique?) → newFacets[]` |
| `removeFacet` | Pure function | `removeFacet(facets[], facet) → newFacets[]` |
| `toggleFacets` | Pure function | `toggleFacets(facets[], facets[]) → newFacets[]` |
| `parseSearchState` | Pure function | Parses URL into search state object |
| `formatSearchState` | Pure function | Serializes search state into URL |
| `sendAnalyticsEvent` | Function | Dispatch analytics events |
| `useAnalyticsEvent` | Hook | Subscribe to analytics events |
**Common mistake:** `setFacet` and `removeFacet` are NOT methods on the `useSearch()` return object. They are standalone pure functions that take and return facet arrays.
references/section-overrides-and-custom-sections.md
---
name: faststore-overrides
description: How to override FastStore native sections and inner components using getOverriddenSection, and how to create brand-new custom sections. Use when customizing existing sections (Navbar, Alert, ProductDetails, etc.), replacing inner component slots (BuyButton, Icon, etc.), adding CSS classes to a section, or creating new sections that integrate with VTEX Headless CMS.
metadata:
author: vtex
version: "1.0"
---
# FastStore Section Overrides & Custom Sections
FastStore provides global sections with defaults that every store typically needs. You can customize them by overriding their inner components or styles, or create entirely new sections.
**Key rule:** Always use `getOverriddenSection` from `@faststore/core` when overriding a section. Never rewrite an entire native section from scratch if overriding is sufficient.
## Section Registry — `src/components/index.tsx`
This is the **single entry point** where FastStore discovers all custom and overridden sections. It must use a **default export** (not named exports).
```tsx
// src/components/index.tsx
import CustomIconsAlert from "./sections/CustomIconsAlert/CustomIconsAlert";
import AlertWithImage from "./sections/AlertWithImage/AlertWithImage";
import CustomProductDetails from "./sections/CustomProductDetails/CustomProductDetails";
import CustomNewsletter from "./sections/CustomNewsletter/CustomNewsletter";
import ContactForm from "./ContactForm/ContactForm";
const sections = {
// New section — unique name, must also exist in cms/faststore/components/cms_component__customIconsAlert.jsonc
CustomIconsAlert,
// New section — alert with image instead of icon. must also exist in cms/faststore/components/cms_component__alertWithImage.jsonc
AlertWithImage,
// Override — key matches native section name, replacing it everywhere
ProductDetails: CustomProductDetails,
// New section — contact form backed by a third-party GraphQL mutation. must also exist in cms/faststore/components/cms_component__contactForm.jsonc
ContactForm,
// New section — custom newsletter with analytics. must also exist in cms/faststore/components/cms_component__customNewsLetter.jsonc
CustomNewsletter,
};
export default sections;
```
- **Override a native section**: Use the exact native name as the key (e.g., `ProductDetails`). The custom component replaces it on all pages.
- **Add a new section**: Use a unique new name as the key (e.g., `ContactForm`). Also define a CMS schema in `cms/faststore/components/cms_component__<sectionName>.jsonc`.
## Pattern 1: Override with Custom CSS Class Only
Use when you only need to restyle a native section, not change its inner components.
```tsx
// src/components/sections/CustomIconsAlert/CustomIconsAlert.tsx
import { getOverriddenSection, AlertSection } from "@faststore/core";
import styles from "./custom-icons-alert.module.scss";
const CustomIconsAlert = getOverriddenSection({
Section: AlertSection,
className: styles.customIconsAlert, // Added to the section root element
// No `components` key needed — only styling changes
});
export default CustomIconsAlert;
```
## Pattern 2: Override an Inner Component
Use when you need to replace a specific sub-component within a native section.
```tsx
// src/components/sections/CustomProductDetails/CustomProductDetails.tsx
import { getOverriddenSection, ProductDetailsSection } from "@faststore/core";
import { BuyButtonWithDetails } from "../../BuyButtonWithDetails/BuyButtonWithDetails";
const CustomProductDetails = getOverriddenSection({
Section: ProductDetailsSection,
components: {
// Key must match the slot name in the native section
BuyButton: {
Component: BuyButtonWithDetails, // Replaces native BuyButton
},
},
});
export default CustomProductDetails;
```
## Pattern 3: Override with Dynamic Props (Memoized)
Use when the override depends on props from the CMS or parent. Memoize with `useMemo` to avoid creating a new component type on every render (which would unmount/remount the subtree).
```tsx
// src/components/sections/AlertWithImage/AlertWithImage.tsx
import { useMemo } from "react";
import { AlertSection, getOverriddenSection } from "@faststore/core";
import { Image_unstable as Image } from "@faststore/core/experimental";
import styles from "./alert-with-image.module.scss";
interface AlertWithImageProps extends Omit<
React.ComponentProps<typeof AlertSection>,
"icon"
> {
src: string;
alt: string;
}
export default function AlertWithImage(props: AlertWithImageProps) {
const { src, alt, ...otherProps } = props;
const OverriddenAlert = useMemo(
() =>
getOverriddenSection({
Section: AlertSection,
className: styles.alertWithImage,
components: {
Icon: {
Component: () => (
<Image src={props.src} alt={props.alt} width={24} height={24} />
),
},
},
}),
[], // Empty deps — override structure is static
);
return <OverriddenAlert {...otherProps} icon="" />;
}
// <project_root>/src/components/index.tsx
export default {
AlertSection: AlertWithImage,
};
```
## `getOverriddenSection` API
```ts
getOverriddenSection({
Section: NativeSection, // Required — native section from @faststore/core
className?: string, // Optional — CSS class applied to section root
components?: { // Optional — map of slot overrides
[SlotName: string]: {
Component: React.ComponentType, // Replacement component (mutually exclusive with props)
props?: Record<string, any>, // Additional props merged into the slot
},
},
})
// Returns: A React component with the same props as the native section
```
`Component` and `props` are mutually exclusive per slot.
## Creating Brand-New Sections
When no native section fits your needs, create a section from scratch.
### Workflow
Follow the **[Mandatory Workflow for New Custom Sections](./cms-schema-and-section-registration.md#mandatory-workflow-for-new-custom-sections)** in `cms-schema-and-section-registration.md` for the complete step-by-step process.
### `gql` usage restrictions
The `gql` tag from `@faststore/core/api` is **only** for:
- Third-party mutations/queries defined in `src/graphql/thirdParty/`
- Fragment extensions in `src/fragments/`
**Do NOT** use `gql` inside custom section components for standalone queries against the built-in `search`, `product`, or `collection` root queries. This breaks the FastStore CLI GraphQL optimization step. Instead, read data from page context hooks (`usePage()`, `usePLP()`, `usePDP()`).
### Handling client-side data loading
Custom sections on PLP/Search pages that depend on client-side data (facets, full product details) must handle the loading state gracefully:
```tsx
import styles from "./my-section.module.scss";
export default function MySection() {
const context = usePage<PLPContext | SearchPageContext>();
const clientData = (context as any)?.data?.search?.facets;
if (!clientData) return null;
return (
<section className={`section ${styles.mySection}`}>
<div className="layout__content">
{/* Section content */}
</div>
</section>
);
}
```
This is expected behavior: the section renders once with server-only data (no facets), then re-renders after `useProductGalleryQuery` completes and the `PageProvider` context updates with the merged data.
### Example: ContactForm Section
```tsx
// src/components/ContactForm/ContactForm.tsx
import { useCallback, useState } from "react";
import { gql } from "@faststore/core/api";
import { useLazyQuery_unstable as useLazyQuery } from "@faststore/core/experimental";
import {
Button as UIButton,
InputField as UIInputField,
Textarea as UITextArea,
} from "@faststore/ui";
import styles from "./contact-form.module.scss";
// gql tag must be at module scope — FastStore's build pipeline statically extracts it
export const mutation = gql(`
mutation SubmitContactForm($data: ContactFormInput!) {
submitContactForm(input: $data) {
message
}
}
`);
export const ContactForm = () => {
const [submitContactForm, { data, error }] = useLazyQuery(mutation, {
data: { name: "", email: "", subject: "", message: "" },
});
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [subject, setSubject] = useState("");
const [message, setMessage] = useState("");
const onSubmit = useCallback(
(event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
submitContactForm({ data: { name, email, subject, message } });
},
[submitContactForm, name, email, subject, message],
);
return (
<section className={`section ${styles.contactForm}`}>
<div className="layout__content">
<form onSubmit={onSubmit}>
<UIInputField
id="name"
label="Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<UIInputField
id="email"
label="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<UIInputField
id="subject"
label="Subject"
value={subject}
onChange={(e) => setSubject(e.target.value)}
/>
<UITextArea
id="message"
placeholder="Write here your message."
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<UIButton type="submit" variant="primary">
Send
</UIButton>
</form>
</div>
</section>
);
};
export default ContactForm;
```
## Quick Override Reference
```tsx
// Minimal override example
import { NavbarSection, getOverriddenSection } from "@faststore/core";
const MyComponent = () => <p>Overridden Component</p>;
export default {
Navbar: getOverriddenSection({
Section: NavbarSection,
components: {
NavbarHeader: { Component: MyComponent },
},
}),
};
```
See [native-sections-and-overridable-slots](native-sections-and-overridable-slots.md) for the full list of sections and their overridable slot names.
## Debugging Custom Sections
### Reading framework source code
When hooks or context don't behave as expected, **read the framework source code** to understand the actual implementation:
**Source location**: `node_modules/@faststore/core/src/` and `node_modules/@faststore/sdk/src/`
**Key files to investigate**:
- `src/sdk/overrides/PageProvider.tsx` — page context types and `usePage()` implementation
- `src/components/templates/ProductListingPage/ProductListing.tsx` — PLP data merging (how `useProductGalleryQuery` merges into context)
- `src/components/templates/ProductDetailsPage/ProductDetailsPage.tsx` — PDP data merging
- `src/sdk/product/useProductGalleryQuery.ts` — client-side search query (facets source)
**Important**: The `.faststore/` folder is generated and should not be edited, but `node_modules/@faststore/*/src/` is readable and is the **ground truth** for API behavior when documentation is unclear or incomplete.
### Debugging checklist
When a custom section doesn't work as expected:
1. **Before writing code that consumes data from hooks or context**, read the source file of the hook in `node_modules/@faststore/core/src/` or `node_modules/@faststore/sdk/src/` to confirm the actual return type and API
2. **Never assume runtime data shapes from GraphQL schema alone** — GraphQL responses may include `__typename` instead of enum fields, may omit fields not in the selection set, or may restructure data through resolvers
3. **Check both server and client render phases** — data available on server may differ from data after client hydration (see [architecture.md](architecture.md) "Server vs Client Data Split")
references/ui-components-and-data-attributes.md
---
name: faststore-atomic-ui
description: FastStore atomic design system including @faststore/components and @faststore/ui packages, available atoms, molecules, and organisms, and their data-fs-* styling attributes. Use when composing new components with FastStore UI building blocks, looking up which UI components are available (Button, InputField, Modal, ProductCard, etc.), or finding the correct data-fs-* attribute to target for SCSS customization.
metadata:
author: vtex
version: "1.0"
---
# FastStore Atomic Design System
FastStore uses an atomic design system with two packages:
- **`@faststore/components`** — React component implementations without default styles. Components expose `data-fs-*` attributes for styling hooks.
- **`@faststore/ui`** — Re-exports `@faststore/components` and provides SCSS stylesheets. Importing `@faststore/ui` styles adds styling to the components.
`@faststore/ui` uses `data-fs-*` attributes defined by `@faststore/components` in its SCSS to style specific components, avoiding style rule conflicts.
## Always use `@faststore/ui` for composition
When building override components or new sections, always compose with `@faststore/ui` atoms and molecules:
```tsx
import {
Button as UIButton,
InputField as UIInputField,
Textarea as UITextArea,
Modal as UIModal,
Badge as UIBadge,
} from "@faststore/ui";
```
## How Styling Works
Components declare `data-fs-*` attributes on their DOM elements, and `@faststore/ui` targets those in SCSS:
```tsx
// Badge component renders:
<div data-fs-badge data-fs-badge-variant="neutral" data-fs-badge-size="small">
<div data-fs-badge-wrapper>{children}</div>
</div>
```
```scss
// @faststore/ui targets it with:
[data-fs-badge] { /* badge styles */ }
[data-fs-badge][data-fs-badge-variant="neutral"] { /* variant styles */ }
```
You can target these same attributes in your `.module.scss` files to customize components:
```scss
.mySection {
[data-fs-badge][data-fs-badge-variant="neutral"] {
background-color: var(--fs-color-primary-bkg);
}
}
```
## Available Components by Category
### Atoms
`Badge`, `Button`, `Checkbox`, `Icon`, `Input`, `Label`, `Link`, `List`, `Loader`, `Overlay`, `Price`, `Radio`, `RichText`, `Select`, `Skeleton`, `Slider`, `SROnly`, `Textarea`
### Molecules
`Accordion`, `Alert`, `Breadcrumb`, `BuyButton`, `Card`, `Carousel`, `CartItem`, `CheckboxField`, `DiscountBadge`, `Dropdown`, `Gift`, `IconButton`, `InputField`, `LinkButton`, `Modal`, `NavbarLinks`, `OrderSummary`, `Popover`, `ProductCard`, `ProductPrice`, `ProductTitle`, `QuantitySelector`, `RadioField`, `RadioGroup`, `Rating`, `RatingField`, `RegionBar`, `SearchAutoComplete`, `SearchDropdown`, `SearchHistory`, `SearchInputField`, `SearchProducts`, `SearchTop`, `SelectField`, `SkuSelector`, `Table`, `Tag`, `TextareaField`, `Toast`, `Toggle`, `ToggleField`, `Tooltip`
### Organisms
`BannerText`, `CartSidebar`, `EmptyState`, `Filter`, `Hero`, `ImageGallery`, `Navbar`, `NavbarSlider`, `Newsletter`, `OutOfStock`, `PaymentMethods`, `PriceRange`, `ProductComparison`, `ProductGrid`, `ProductShelf`, `RegionModal`, `SearchInput`, `ShippingSimulation`, `SKUMatrix`, `SlideOver`
## Full `data-fs-*` Attribute Reference
See [references/REFERENCE.md](references/REFERENCE.md) for the complete list of all `data-fs-*` styling attributes organized by atom, molecule, and organism.
## Example — Composing a Custom Component
```tsx
import {
Button as UIButton,
InputField as UIInputField,
} from "@faststore/ui";
import styles from "./my-component.module.scss";
export function MyCustomForm() {
return (
<div className={styles.wrapper}>
<UIInputField id="email" label="Email" />
<UIButton variant="primary" type="submit">
Subscribe
</UIButton>
</div>
);
}
```
```scss
// my-component.module.scss
.wrapper {
display: flex;
flex-direction: column;
gap: var(--fs-spacing-3);
// Customize the Button inside this component
[data-fs-button][data-fs-button-variant="primary"] {
border-radius: var(--fs-border-radius-pill);
}
}
```
# Faststore design system.
Faststore uses a ATOMIC design system that can be used by user to compose new components and no need of reimplement features or ensure correct HTML Semantic for search engines.
Therer is two packages:
- @faststore/components: Design system react component implementation without estyling.
- @faststore/ui: Re-exports @faststore/components and also provides sass stylesheets that when imported adds styles to the components.
The `@faststore/ui` uses `data-*` attributes defined by `@faststore/components` in its stylesheet to style specific components without worring about style rules crash override.
Example:
```tsx
// Badge component at @faststore/components
const Badge = forwardRef<HTMLDivElement, BadgeProps>(function Badge(
{
testId = 'fs-badge',
size = 'small',
variant = 'neutral',
counter = false,
'aria-label': ariaLabel,
children,
...otherProps
}: BadgeProps,
ref
) {
return (
<div
ref={ref}
data-fs-badge
aria-label={ariaLabel}
data-fs-badge-variant={counter ? null : variant}
data-fs-badge-size={size}
data-fs-badge-counter={counter}
data-testid={testId}
{...otherProps}
>
<div data-fs-badge-wrapper>{children}</div>
</div>
)
})
export default Badge
```
```sass
<!-- Badge styling under @faststore/ui -->
[data-fs-badge] {
<!-- Badge rules... -->
}
```
---
# `@faststore/components` data attributes reference
This file lists styling hooks used by `@faststore/ui` (primarily `data-fs-*`).
## Atoms
- `Badge`: `data-fs-badge`, `data-fs-badge-counter`, `data-fs-badge-size`, `data-fs-badge-variant`, `data-fs-badge-wrapper`
- `Button`: `data-fs-button`, `data-fs-button-icon`, `data-fs-button-inverse`, `data-fs-button-loading`, `data-fs-button-loading-label`, `data-fs-button-size`, `data-fs-button-variant`, `data-fs-button-wrapper`
- `Checkbox`: `data-fs-checkbox`, `data-fs-checkbox-partial`
- `Icon`: `data-fs-icon`
- `Input`: `data-fs-input`
- `Label`: `data-fs-label`
- `Link`: `data-fs-link`, `data-fs-link-inverse`, `data-fs-link-size`, `data-fs-link-variant`
- `List`: `data-fs-list`, `data-fs-list-marker`
- `Loader`: `data-fs-loader`, `data-fs-loader-item`, `data-fs-loader-variant`
- `Overlay`: `data-fs-overlay`
- `Price`: `data-fs-price`, `data-fs-price-variant`
- `Radio`: `data-fs-radio`
- `RichText`: `data-fs-rich-text`
- `Select`: `data-fs-select`, `data-fs-select-icon`
- `Skeleton`: `data-fs-skeleton`, `data-fs-skeleton-border`, `data-fs-skeleton-shimmer`
- `Slider`: `data-fs-slider`, `data-fs-slider-absolute-values`, `data-fs-slider-range`, `data-fs-slider-thumb`, `data-fs-slider-value-label`, `data-fs-slider-wrapper`
- `SROnly`: `data-fs-sr-only`
- `Textarea`: `data-fs-textarea`, `data-fs-textarea-resize`
## Molecules
- `Accordion`: `data-fs-accordion`, `data-fs-accordion-button`, `data-fs-accordion-item`, `data-fs-accordion-panel`
- `Alert`: `data-fs-alert`, `data-fs-alert-button`, `data-fs-alert-content`, `data-fs-alert-dismissible`, `data-fs-alert-link`, `data-fs-content`
- `Breadcrumb`: `data-fs-breadcrumb`, `data-fs-breadcrumb-divider`, `data-fs-breadcrumb-dropdown-button`, `data-fs-breadcrumb-dropdown-item`, `data-fs-breadcrumb-dropdown-link`, `data-fs-breadcrumb-dropdown-menu`, `data-fs-breadcrumb-is-desktop`, `data-fs-breadcrumb-item`, `data-fs-breadcrumb-link`, `data-fs-breadcrumb-list`, `data-fs-breadcrumb-list-item`, `data-fs-content`, `data-fs-dropdown-item-icon`
- `BuyButton`: `data-fs-buy-button`
- `Card`: `data-fs-card`, `data-fs-card-body`, `data-fs-card-header`, `data-fs-card-title`
- `Carousel`: `data-fs-carousel`, `data-fs-carousel-bullet`, `data-fs-carousel-bullets`, `data-fs-carousel-control`, `data-fs-carousel-controls`, `data-fs-carousel-item`, `data-fs-carousel-item-visible`, `data-fs-carousel-track`, `data-fs-carousel-track-container`, `data-fs-carousel-variant`
- `CartItem`: `data-fs-cart-item`, `data-fs-cart-item-actions`, `data-fs-cart-item-content`, `data-fs-cart-item-image`, `data-fs-cart-item-prices`, `data-fs-cart-item-remove-button`, `data-fs-cart-item-skus`, `data-fs-cart-item-summary`, `data-fs-cart-item-title`
- `CheckboxField`: `data-fs-checkbox-field`, `data-fs-checkbox-field-alignment`, `data-fs-checkbox-field-content`, `data-fs-checkbox-field-error`, `data-fs-checkbox-field-error-message`, `data-fs-checkbox-field-label`
- `DiscountBadge`: `data-fs-discount-badge`, `data-fs-discount-badge-variant`
- `Dropdown`: `data-fs-dropdown-button`, `data-fs-dropdown-item`, `data-fs-dropdown-menu`, `data-fs-dropdown-menu-size`, `data-fs-dropdown-overlay`
- `Gift`: `data-fs-gift`, `data-fs-gift-content`, `data-fs-gift-icon`, `data-fs-gift-image`, `data-fs-gift-product-summary`, `data-fs-gift-product-title`, `data-fs-gift-wrapper`
- `IconButton`: `data-fs-button`, `data-fs-icon-button`
- `InputField`: `data-fs-input-field`, `data-fs-input-field-actionable`, `data-fs-input-field-error`, `data-fs-input-field-error-message`
- `LinkButton`: `data-fs-button`, `data-fs-button-disabled`, `data-fs-button-icon`, `data-fs-button-inverse`, `data-fs-button-size`, `data-fs-button-variant`, `data-fs-button-wrapper`, `data-fs-link-button`
- `Modal`: `data-fs-modal`, `data-fs-modal-body`, `data-fs-modal-content`, `data-fs-modal-footer`, `data-fs-modal-footer-actions`, `data-fs-modal-footer-actions-direction`, `data-fs-modal-footer-actions-wrap`, `data-fs-modal-header`, `data-fs-modal-header-close-button`, `data-fs-modal-header-description`, `data-fs-modal-header-title`, `data-fs-modal-state`
- `NavbarLinks`: `data-fs-navbar-links`, `data-fs-navbar-links-list`, `data-fs-navbar-links-list-item`
- `OrderSummary`: `data-fs-order-summary`, `data-fs-order-summary-discount`, `data-fs-order-summary-discount-label`, `data-fs-order-summary-discount-value`, `data-fs-order-summary-subtotal`, `data-fs-order-summary-subtotal-label`, `data-fs-order-summary-subtotal-value`, `data-fs-order-summary-taxes-label`, `data-fs-order-summary-total`, `data-fs-order-summary-total-label`, `data-fs-order-summary-total-value`
- `Popover`: `data-fs-popover`, `data-fs-popover-content`, `data-fs-popover-header`, `data-fs-popover-header-dismiss-button`, `data-fs-popover-header-title`, `data-fs-popover-indicator`, `data-fs-popover-placement`
- `ProductCard`: `data-fs-product-card`, `data-fs-product-card-actions`, `data-fs-product-card-badge`, `data-fs-product-card-bordered`, `data-fs-product-card-content`, `data-fs-product-card-delivery-promise-badge`, `data-fs-product-card-delivery-promise-badge-availability`, `data-fs-product-card-delivery-promise-badges`, `data-fs-product-card-heading`, `data-fs-product-card-image`, `data-fs-product-card-prices`, `data-fs-product-card-sponsored-label`, `data-fs-product-card-taxes-label`, `data-fs-product-card-title`, `data-fs-product-card-variant`
- `ProductPrice`: `data-fs-product-price`
- `ProductTitle`: `data-fs-product-title`, `data-fs-product-title-addendum`, `data-fs-product-title-header`
- `QuantitySelector`: `data-fs-quantity-selector`
- `RadioField`: `data-fs-radio-field`
- `RadioGroup`: `data-fs-radio-group-option`, `data-fs-radio-option-item`
- `Rating`: `data-fs-rating`, `data-fs-rating-actionable`, `data-fs-rating-button`, `data-fs-rating-icon-outline`, `data-fs-rating-icon-wrapper`, `data-fs-rating-item`
- `RatingField`: `data-fs-rating-field`, `data-fs-rating-field-disabled`, `data-fs-rating-field-error`, `data-fs-rating-field-error-message`, `data-fs-rating-field-input`, `data-fs-rating-field-label`
- `RegionBar`: `data-fs-region-bar`, `data-fs-region-bar-filter`, `data-fs-region-bar-filter-message`, `data-fs-region-bar-location`, `data-fs-region-bar-location-city`, `data-fs-region-bar-location-message`, `data-fs-region-bar-location-postal-code`, `data-fs-region-bar-message`, `data-fs-region-bar-postal-code`
- `SearchAutoComplete`: `data-fs-search-auto-complete`, `data-fs-search-auto-complete-item`, `data-fs-search-auto-complete-item-icon`, `data-fs-search-auto-complete-item-link`, `data-fs-search-auto-complete-item-suggestion`
- `SearchDropdown`: `data-fs-search-dropdown`, `data-fs-search-dropdown-loading-text`
- `SearchHistory`: `data-fs-search-history`, `data-fs-search-history-header`, `data-fs-search-history-item`, `data-fs-search-history-item-icon`, `data-fs-search-history-item-link`, `data-fs-search-history-title`
- `SearchInputField`: `data-fs-search-input-field`, `data-fs-search-input-field-input`
- `SearchProducts`: `data-fs-product-item-control-input`, `data-fs-search-product-item`, `data-fs-search-product-item-content`, `data-fs-search-product-item-control`, `data-fs-search-product-item-control-actions`, `data-fs-search-product-item-control-actions-desktop`, `data-fs-search-product-item-control-actions-mobile`, `data-fs-search-product-item-control-badge`, `data-fs-search-product-item-control-content`, `data-fs-search-product-item-image`, `data-fs-search-product-item-link`, `data-fs-search-product-item-prices`, `data-fs-search-product-item-title`, `data-fs-search-products`, `data-fs-search-products-header`, `data-fs-search-products-title`
- `SearchProvider`: none
- `SearchTop`: `data-fs-search-top`, `data-fs-search-top-header`, `data-fs-search-top-item`, `data-fs-search-top-item-badge`, `data-fs-search-top-item-link`, `data-fs-search-top-title`
- `SelectField`: `data-fs-select-field`, `data-fs-select-field-label`
- `SkuSelector`: `data-fs-sku-selector`, `data-fs-sku-selector-checked`, `data-fs-sku-selector-disabled`, `data-fs-sku-selector-list`, `data-fs-sku-selector-option`, `data-fs-sku-selector-option-color`, `data-fs-sku-selector-option-image`, `data-fs-sku-selector-option-link`, `data-fs-sku-selector-title`, `data-fs-sku-selector-variant`
- `Table`: `data-fs-table`, `data-fs-table-body`, `data-fs-table-cell`, `data-fs-table-cell-align`, `data-fs-table-content`, `data-fs-table-footer`, `data-fs-table-head`, `data-fs-table-row`, `data-fs-table-variant`
- `Tag`: `data-fs-tag`, `data-fs-tag-icon-button`, `data-fs-tag-label`
- `TextareaField`: `data-fs-textarea-field`, `data-fs-textarea-field-error`, `data-fs-textarea-field-error-message`, `data-fs-textarea-field-label`
- `Toast`: `data-fs-toast`, `data-fs-toast-content`, `data-fs-toast-icon-container`, `data-fs-toast-message`, `data-fs-toast-title`, `data-fs-toast-visible`
- `Toggle`: `data-fs-toggle`, `data-fs-toggle-knob`, `data-fs-toggle-variant`
- `ToggleField`: `data-fs-toggle-field`, `data-fs-toggle-field-label`
- `Tooltip`: `data-fs-tooltip`, `data-fs-tooltip-content`, `data-fs-tooltip-dismiss-button`, `data-fs-tooltip-dismissible`, `data-fs-tooltip-indicator`, `data-fs-tooltip-placement`, `data-fs-tooltip-wrapper`
## Organisms
- `BannerText`: `data-fs-banner-text`, `data-fs-banner-text-color-variant`, `data-fs-banner-text-content`, `data-fs-banner-text-heading`, `data-fs-banner-text-link`, `data-fs-banner-text-variant`, `data-fs-content`
- `CartSidebar`: `data-fs-cart-sidebar`, `data-fs-cart-sidebar-footer`, `data-fs-cart-sidebar-list`, `data-fs-cart-sidebar-title`
- `EmptyState`: `data-fs-content`, `data-fs-empty-state`, `data-fs-empty-state-bkg-color`, `data-fs-empty-state-title`, `data-fs-empty-state-variant`
- `Filter`: `data-fs-filter`, `data-fs-filter-accordion`, `data-fs-filter-accordion-item`, `data-fs-filter-accordion-item-description`, `data-fs-filter-facet-range`, `data-fs-filter-list`, `data-fs-filter-list-item`, `data-fs-filter-list-item-badge`, `data-fs-filter-list-item-checkbox`, `data-fs-filter-list-item-label`, `data-fs-filter-list-item-radio`, `data-fs-filter-slider`, `data-fs-filter-slider-content`, `data-fs-filter-slider-footer`, `data-fs-filter-slider-footer-button-apply`, `data-fs-filter-slider-footer-button-clear`, `data-fs-filter-slider-title`, `data-fs-filter-title`
- `Hero`: `data-fs-content`, `data-fs-hero`, `data-fs-hero-color-variant`, `data-fs-hero-heading`, `data-fs-hero-icon`, `data-fs-hero-image`, `data-fs-hero-info`, `data-fs-hero-subtitle`, `data-fs-hero-title`, `data-fs-hero-variant`, `data-fs-hero-wrapper`
- `ImageGallery`: `data-fs-image-gallery`, `data-fs-image-gallery-selector`, `data-fs-image-gallery-selector-control`, `data-fs-image-gallery-selector-control-button`, `data-fs-image-gallery-selector-elements`, `data-fs-image-gallery-selector-thumbnail`
- `Navbar`: `data-fs-content`, `data-fs-navbar`, `data-fs-navbar-buttons`, `data-fs-navbar-header`, `data-fs-navbar-row`, `data-fs-navbar-scroll`, `data-fs-navbar-search-expanded`
- `NavbarSlider`: `data-fs-navbar-slider`, `data-fs-navbar-slider-content`, `data-fs-navbar-slider-footer`, `data-fs-navbar-slider-header`
- `Newsletter`: `data-fs-content`, `data-fs-newsletter`, `data-fs-newsletter-addendum`, `data-fs-newsletter-color-variant`, `data-fs-newsletter-content`, `data-fs-newsletter-form`, `data-fs-newsletter-header`, `data-fs-newsletter-header-description`, `data-fs-newsletter-header-title`
- `OutOfStock`: `data-fs-out-of-stock`, `data-fs-out-of-stock-button`, `data-fs-out-of-stock-message`, `data-fs-out-of-stock-title`
- `PaymentMethods`: `data-fs-payment-methods`, `data-fs-payment-methods-flag`, `data-fs-payment-methods-flags`, `data-fs-payment-methods-title`
- `PriceRange`: `data-fs-price-range`, `data-fs-price-range-inputs`
- `ProductComparison`: `data-fs-dropdown-filter-selected`, `data-fs-product-comparison-container`, `data-fs-product-comparison-dropdown-button`, `data-fs-product-comparison-dropdown-item-filter-type`, `data-fs-product-comparison-dropdown-item-filter-type-text`, `data-fs-product-comparison-dropdown-menu-content`, `data-fs-product-comparison-filters`, `data-fs-product-comparison-filters-sort-label`, `data-fs-product-comparison-row-header`, `data-fs-product-comparison-row-header-button`, `data-fs-product-comparison-row-header-button-description`, `data-fs-product-comparison-row-header-button-title`, `data-fs-product-comparison-row-label`, `data-fs-product-comparison-row-text`, `data-fs-product-comparison-selection-warning-label`, `data-fs-product-comparison-sidebar`, `data-fs-product-comparison-sidebar-header-title`, `data-fs-product-comparison-toggle-field-mobile`, `data-fs-product-comparison-toolbar`, `data-fs-product-comparison-toolbar-image`, `data-fs-product-comparison-toolbar-image-more`, `data-fs-product-comparison-trigger`, `data-fs-product-comparison-trigger-checkbox-field`
- `ProductGrid`: `data-fs-product-grid`, `data-fs-product-grid-item`
- `ProductShelf`: `data-fs-content`, `data-fs-product-shelf`, `data-fs-product-shelf-item`, `data-fs-product-shelf-items`
- `RegionModal`: `data-fs-region-modal`, `data-fs-region-modal-input`, `data-fs-region-modal-link`
- `SearchInput`: `data-fs-search-input`, `data-fs-search-input-dropdown-visible`
- `ShippingSimulation`: `data-fs-shipping-simulation`, `data-fs-shipping-simulation-empty`, `data-fs-shipping-simulation-header`, `data-fs-shipping-simulation-link`, `data-fs-shipping-simulation-location`, `data-fs-shipping-simulation-option-carrier`, `data-fs-shipping-simulation-option-estimate`, `data-fs-shipping-simulation-subtitle`, `data-fs-shipping-simulation-title`
- `SKUMatrix`: `data-fs-sku-matrix`, `data-fs-sku-matrix-sidebar`, `data-fs-sku-matrix-sidebar-cell-image`, `data-fs-sku-matrix-sidebar-footer`, `data-fs-sku-matrix-sidebar-table-action`, `data-fs-sku-matrix-sidebar-table-cell-quantity-selector`, `data-fs-sku-matrix-sidebar-table-price`, `data-fs-sku-matrix-sidebar-title`
- `SlideOver`: `data-fs-modal`, `data-fs-slide-over`, `data-fs-slide-over-direction`, `data-fs-slide-over-header`, `data-fs-slide-over-header-icon`, `data-fs-slide-over-size`, `data-fs-slide-over-state`
## Notes
- This reference intentionally excludes non-styling helpers such as `data-testid`.
- A few non `data-fs-*` hooks also exist in code (for example `data-value`, `data-type`, `data-quantity`, `data-icon`, `data-quantity-selector-*`) and may be used for behavior or testing depending on the component.
scripts/cms-sync.sh
#!/usr/bin/env bash
#
# DEPRECATED — Headless CMS schema publishing
# Prefer running from the project root (global VTEX CLI):
# vtex content generate-schema -o cms/faststore/schema.json
# vtex content upload-schema cms/faststore/schema.json
# Do not use yarn/npm cms-sync or faststore cms-sync for this workflow.
# See references/cms-schema-and-section-registration.md and skill.md.
#
# Generate final schema.json for Headless CMS (canonical flags — matches skill.md)
vtex content generate-schema -o cms/faststore/schema.json
# Upload final schema.json to Headless CMS (expects interactive prompts or use expect — see reference)
expect -c 'spawn vtex content upload-schema cms/faststore/schema.json; expect "store ID"; send "faststore\r"; expect "uploaded with"; send "y\r"; expect "Are you sure"; send "y\r"; expect eof' 2>&1
SKILL.md
---
name: faststore-storefront
description: "Core coding rules and workflow for developing VTEX FastStore storefronts. Use when starting any FastStore development task, writing TypeScript/React components, creating section overrides, extending the BFF, or styling. Covers all primary conventions, safety rules, and the development workflow used across every FastStore project."
---
# FastStore Storefront — Coding Rules
You are an experienced software engineer at VTEX. Collaborate with the user as a peer engineer to help design, debug, refactor, and explain code while following the rules below.
## Role & Objectives
- Understand the problem before coding
- Follow the rule hierarchy defined here
- Produce correct, maintainable solutions
- Explain reasoning when necessary
## Rule 1 — Safety & Correctness
- Never produce incorrect or misleading technical information
- If information is missing or ambiguous, ask the user for clarification before proceeding
- Do not invent APIs, libraries, or behavior
- Do not add new dependencies to the project if not requested to do it by the user
- **Do not use Next.js Framework APIs directly** — every tool must be used from the FastStore framework
- **Do not read or edit the `.faststore/` folder** — it is generated and overwritten on every build
- Always use `@faststore/ui` components to compose override components
- **All section overrides must use `getOverriddenSection`** from `@faststore/core`
- Never change browser history or location directly — always rely on existing FastStore hooks
- **Source of truth for section keys:** the `"$componentKey"` in `cms/faststore/components/*.jsonc` must match the **object key** in `<project_root>/src/components/index.tsx` (default export). Do not treat `cms/faststore/schema.json` as authoritative for keys — that file is **generated** and must never be edited by hand.
- Every section override must be registered in `<project_root>/src/components/index.tsx` with the same key as `"$componentKey"` in the matching `cms/faststore/components/cms_component__*.jsonc`.
- The file `<project_root>/src/components/index.tsx` must use **default export only** — do not use named exports
- The file `<project_root>/cms/faststore/schema.json` must not be edited. It is always regenerated by `vtex content generate-schema`
- **If the `.faststore/` directory gets into a broken state** (e.g., after a failed GraphQL optimization), delete it with `rm -rf .faststore` and restart `yarn dev`. The CLI regenerates it from scratch.
- **Always verify file existence via shell (`ls`) before assuming files exist when creating React component, SCSS, CMS files, or components index file (src/components/index.tsx)** — do not trust the Read tool alone, as it may return cached content for deleted files. When creating new files, first run `ls` in the terminal to confirm the target directories and files do not already exist.
- Before creating ANY new file (component, SCSS, CMS schema):
1. **MANDATORY**: Run `ls -la <directory>` to verify:
- Directory structure exists
- No conflicting files with same name
- Correct location for file type
2. **For CMS components**: Check both `src/components/` AND `cms/faststore/components/`
Example workflow:
```bash
# Before creating DailyOffers component
ls -la src/components/DailyOffers # Should not exist
ls -la cms/faststore/components | grep -i daily # Check for existing
```
## Rule 2 — Requirement Adherence
- Follow the user's request exactly
- Use **TypeScript**
- All code must follow **React 18**
- Follow FastStore framework architecture — never work around it
## Rule 3 — Context Awareness
- Use all context provided by the user (code snippets, architecture, errors)
- Do not ignore relevant information
- Prefer components from `@faststore/components` or `@faststore/ui`
## Rule 4 — Minimalism
- Do not over-engineer
- Provide the simplest solution that satisfies the requirements
## Rule 5 — Explanation (When Useful)
- Briefly explain reasoning for complex decisions
- Focus on practical insights useful to another developer
## Code Output Rules
- Never create or modify code inside the `.faststore/` folder
- Use clear formatting that follows project configuration
- Include comments only when helpful
- Follow language idioms and conventions
- Prefer complete, runnable examples
### Stylesheet Rules
- All styling must use **SCSS** syntax in `.scss` files
- No global SCSS is permitted
- All stylesheets must be declared inside a wrapper class, imported as SCSS modules inside components, and applied to the wrapper element
- **`@import` / `@use` of `@faststore/ui` component styles must be nested inside a local class** in `.module.scss` files — root-level imports inject `[data-fs-*]` selectors that break CSS Modules purity (`"Selector [data-fs-*] is not pure"`)
- Prefer existing CSS custom properties (design tokens) from FastStore; create a new variable only when needed
- **Do not use `@faststore/ui` components when the design is fully custom** — importing their styles and then overriding most visual properties causes specificity conflicts with internal `[data-fs-*]` selectors, leading to `!important` escalation. Use native HTML elements with custom SCSS instead. Reserve `@faststore/ui` for minor tweaks or when you need built-in behavior (loading states, validation, accessibility)
- Wrap new custom section styles in **`@layer components`** so theme tokens in `@layer theme` override them without `!important` — matching the cascade order of native sections
### Prerequisite: VTEX CLI (global)
Assume **[VTEX CLI](https://developers.vtex.com/docs/guides/vtex-io-documentation-vtex-io-cli-install)** is installed globally. Use **`vtex` directly** (for example `vtex content …`). Do **not** document or suggest `npx vtex` for these flows.
### Headless CMS schema rule (no legacy `cms-sync`)
**Do not** recommend `yarn cms-sync`, `npm run cms-sync`, `faststore cms-sync`, or any other **legacy `cms-sync` flow** to publish or refresh the **Headless CMS** schema. For schema, the supported path is **`vtex content generate-schema`** and **`vtex content upload-schema`** (see below).
### CMS schema workflow — follow through in the same session
After **every** change to `cms/faststore/components/*.jsonc` or `cms/faststore/pages/*.jsonc`, complete this sequence **before considering the task done**:
1. **Generate** — from the project root, run:
```bash
vtex content generate-schema -o cms/faststore/schema.json
```
2. **Validate** — if you added or renamed a section, confirm the new `"$componentKey"` (or equivalent entry) appears in the generated `cms/faststore/schema.json`. If it is missing, fix the JSONC or registration in `src/components/index.tsx` and regenerate — **never** patch `schema.json` manually.
3. **Upload** — in the **same session**, use the non-interactive command:
```bash
# The CLI expects "faststore" as the schema suffix (not the storeId from discovery.config.js)
# This results in $id = {discovery.storeId}.faststore (e.g., brandless.faststore)
# Single quotes prevent Tcl from interpreting $id and other $ tokens in CLI output
expect -c 'spawn vtex content upload-schema cms/faststore/schema.json; expect "store ID"; send "faststore\r"; expect -re "uploaded|confirm"; send "y\r"; expect -re "Are you sure|confirm"; send "y\r"; expect eof' 2>&1
```
4. **Report** — state clearly whether upload succeeded. If the CLI prompts for **login**, **store ID**, or **confirmation**, paste the **exact prompt or error** and specify the **human next step** (e.g. run `vtex login`, confirm the account matches `discovery.config.js` → `api.storeId`) or point to the **non-interactive `expect` example** in [references/cms-schema-and-section-registration.md](references/cms-schema-and-section-registration.md).
**What upload does vs. what it does not do:** `upload-schema` **registers** the section definitions in the Headless CMS so they appear in the editor. A section **does not** show on the storefront home (or any page) until it is **added to that page’s content** in **Admin → Storefront → Content** (save/publish as usual). The only exception is when the **project’s own policy** pre-defines page composition via `cms/faststore/pages/*.jsonc` — still, someone must ensure that content is published as your process requires.
Canonical commands (project root):
```bash
vtex content generate-schema -o cms/faststore/schema.json
vtex content upload-schema cms/faststore/schema.json
```
## Workflow
Follow this process for every request:
1. **Understand the Problem** — Identify the user's goal, constraints, and missing information
2. **Analyze** — Determine the root problem and consider approaches
3. **Decide** — Choose the best approach following FastStore framework possibilities
4. **Provide** — Code + explanation (if needed) + alternatives (optional)
5. **Review** — After finishing, verify:
- No code produced inside `.faststore/` folder
- Code composed of `@faststore/components` atoms and molecules
- If CMS JSONC or pages JSONC changed: `generate-schema` was run, `schema.json` was validated (new `$componentKey` when applicable), `upload-schema` was attempted in-session, and the outcome (success or exact CLI prompt/error + next step) was reported
- For new CMS sections: it is clear that **Admin → Storefront → Content** (or project `pages` JSONC policy) is still required for the section to appear on a live page
## Response Format
When appropriate, structure responses as:
**Problem Understanding**
Short summary of what the user needs.
**Solution**
Code or steps.
**Explanation**
Why this solution works.
**Optional Improvements**
Better patterns, optimizations, etc.
## Reference Files
Load these on demand based on what the task requires. Do not load all of them upfront.
| File | Load when… |
| -------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [references/project-structure-routes-and-config.md](references/project-structure-routes-and-config.md) | Mapping the repo: what belongs in `src/` vs generated `.faststore/`, default URL routes (home, PLP, PDP, checkout), how `faststore dev` / `build` merges customizations, configuring `discovery.config.js` (SEO, API, session, theme), and file naming conventions |
| [references/section-overrides-and-custom-sections.md](references/section-overrides-and-custom-sections.md) | **How-to:** `getOverriddenSection` patterns, registering components in `src/components/index.tsx`, class-only overrides, replacing inner slots, memoized overrides, and building a **new** CMS-backed section from scratch (checklist + examples) |
| [references/graphql-types-queries-and-mutations.md](references/graphql-types-queries-and-mutations.md) | **Read-only API catalog:** built-in root `Query` / `Mutation` fields, enums (e.g. `StoreSort`), and field lists for types like `StoreProduct`, `StoreCart`, `StoreSession` — use when writing queries or checking what the platform already exposes (**not** for adding custom resolvers) |
| [references/extending-graphql-with-custom-resolvers.md](references/extending-graphql-with-custom-resolvers.md) | **Implementation guide:** adding fields under `src/graphql/vtex/` or new operations under `src/graphql/thirdParty/`, wiring resolvers, `Server*` / `Client*` fragments, and consuming data with `usePDP` / `useQuery` / `useLazyQuery` |
| [references/scss-styling-and-design-tokens.md](references/scss-styling-and-design-tokens.md) | SCSS module rules (wrapper class, no global SCSS), theming and CSS variables in `src/themes/custom-theme.scss`, and styling overrides that target inner UI structure |
| [references/cms-schema-and-section-registration.md](references/cms-schema-and-section-registration.md) | VTEX Headless CMS: `cms_component__*.jsonc` + `index.tsx` as source of truth, generated `schema.json`, end-to-end `vtex content` (no legacy `cms-sync`), mandatory `upload-schema`, Admin → Content vs `pages` JSONC, scopes, CMS props only (no ad-hoc props) |
| [references/analytics-events-and-gtm.md](references/analytics-events-and-gtm.md) | `@faststore/sdk` analytics: `sendAnalyticsEvent`, `useAnalyticsEvent` / handler components, and setting `gtmContainerId` in `discovery.config.js` |
| [references/injecting-head-scripts-and-meta-tags.md](references/injecting-head-scripts-and-meta-tags.md) | Custom `<head>` content via `src/scripts/ThirdPartyScripts.tsx` (verification meta tags, inline scripts, Partytown) — **not** the primary place for GTM; use `discovery.config.js` (see analytics reference) |
| [references/native-sections-and-overridable-slots.md](references/native-sections-and-overridable-slots.md) | **Lookup only:** list of built-in global sections (e.g. `Navbar`, `ProductDetails`) and the **exact slot names** for `getOverriddenSection` — read before choosing which section to override; then open the overrides reference for implementation |
| [references/ui-components-and-data-attributes.md](references/ui-components-and-data-attributes.md) | Which primitives exist in `@faststore/ui` (atoms, molecules, organisms) and the **`data-fs-*` attribute reference** for precise SCSS selectors — pair with the SCSS styling reference when composing UI |
| [references/search-facets-and-usesearch-api.md](references/search-facets-and-usesearch-api.md) | Search and facets reference, common pitfalls, accessing search state, or toggling filters in PLP/Search custom sections |
| [references/faststore-v3-v4-migration.md](references/faststore-v3-v4-migration.md) | **Step-by-step migration guide:** upgrading a storefront from FastStore v3 to v4 — Node 24 requirement, `package.json` changes, `discovery.config.js` plain-config rule, SCSS `@import` → `@use`/`@forward` migration, GraphQL import migration, v3 patch assessment, verification, and post-migration CMS sync (Headless CMS and Content Platform cases) |