Metafield
Description
Renders the value of a single metafield from an entity you supply. The component is purely presentational: it performs no data fetching and reads no entity contexts, which makes it usable anywhere the data is available — a product detail page, a category grid, a related-products shelf, or the footer.
Its only ambient input is the active theme's declaration of the field, projected into the app context from the theme's theme.json. That declaration supplies the field's display name, its placeholder default, and its customer-visibility flag.
Import
import { Metafield } from '@components/common/metafield/index.js';
Usage
import { Metafield } from '@components/common/metafield/index.js';
import { useProduct } from '@components/frontStore/catalog/ProductContext.js';
export default function ProductMaterial() {
const product = useProduct();
return (
<Metafield
owner="product"
entity={product}
namespace="mytheme"
fieldKey="material"
as="p"
className="product__material"
/>
);
}
export const layout = { areaId: 'productNameAfter', sortOrder: 10 };
Props
| Name | Type | Default | Description |
|---|---|---|---|
| owner | string | - | Owner entity type (required). product, category, collection, customer, order, shop, blog_post, blog_category — an open set. |
| namespace | string | - | The field's namespace (required). |
| fieldKey | string | - | The field's key (required). |
| entity | { uuid?, metafields? } | null | undefined | - | Required. The entity object carrying the metafields selection. There is no context fallback — see below. |
| defaultValue | any | - | Per-call-site fallback rendered when the value is unset. Takes precedence over the declaration's placeholder. |
| render | (value, meta) => ReactNode | - | Custom renderer. Required for anything that is not a string or a number. |
| as | keyof JSX.IntrinsicElements | 'span' | Tag used by the built-in scalar renderer. Ignored when render is supplied. |
| className | string | - | Class applied by the built-in scalar renderer. Ignored when render is supplied. |
entity is required
<Metafield> never looks up the entity for you. It does not call useProduct(), it does not read a category or collection context, and it issues no query of its own. You pass the object, and the object must carry the field's data.
// Detail page — pass the page's entity.
const product = useProduct();
<Metafield owner="product" entity={product} namespace="mytheme" fieldKey="material" />
// Grid or shelf — no per-item context exists, so pass each item.
{products.map((item) => (
<Metafield key={item.uuid} owner="product" entity={item}
namespace="mytheme" fieldKey="material" />
))}
// Shop-level field — pass the setting object from the enclosing master's query.
<Metafield owner="shop" entity={setting} namespace="mytheme" fieldKey="tagline" />
A nullish entity is tolerated at runtime — the resolution simply falls through to the default — so loading states and optional data do not crash the page.
For this to produce anything, the page's GraphQL query must select the field data:
currentProduct {
uuid
metafields { namespace key type value }
}
The default productView and categoryView queries already do. If you override the master component with your own query, carry the selection across or every <Metafield> on the page falls back to its default.
Resolution order
| Condition | Rendered |
|---|---|
Declared with visibleToCustomer: false | Nothing — not the default, not the placeholder. |
| Value is set on the entity | The value. |
Value is unset and defaultValue was passed | defaultValue. |
Value is unset and no defaultValue | The declaration's appearance.placeholder. |
| Neither exists | Nothing. |
| Stored type does not match the declared type | The default, plus a development-mode console warning. |
A field declared visibleToCustomer: false is dropped from the customer-facing GraphQL response. If the component fell back to the default for it, a private field's placeholder text would be published on the storefront. So visibility is checked first, before the value and before any fallback.
Example: rendering a non-scalar value
The built-in renderer only handles strings and numbers — lists, groups, booleans and rich text need a render function. (Without one, nothing renders and a development-mode warning is logged.)
<Metafield
owner="product"
entity={product}
namespace="mytheme"
fieldKey="badges"
render={(value) =>
Array.isArray(value) ? (
<div className="product__badges">
{value.map((badge, i) => (
<span key={i} className={`badge badge--${badge.tone}`}>
{badge.label}
</span>
))}
</div>
) : null
}
/>
Example: using meta
The second argument to render describes the field:
| Key | Type | Description |
|---|---|---|
| type | string | Normalized (lowercase) field type — declared type first, stored type otherwise. |
| isList | boolean | Present when the declaration marks the field as a list. |
| name | string | The declaration's display name, when declared. |
| placeholder | string | The declaration's appearance.placeholder, when declared. |
| isDefault | boolean | true when what you are rendering is a fallback rather than stored data. |
<Metafield
owner="product"
entity={product}
namespace="mytheme"
fieldKey="care"
render={(value, meta) => (
<p className={meta.isDefault ? 'text-muted-foreground italic' : undefined}>
{String(value)}
</p>
)}
/>
Page builder behaviour
Inside the page-builder iframe the component gains a violet "Live data" hover outline; clicking it opens an informational drawer describing the field, its definition status, and where its value is edited. A declared field that renders nothing on the storefront — hidden, or unset with no default — shows a dashed ghost chip instead, so it stays discoverable on the canvas.
None of this affects the storefront: the first render is always the bare production element, and the chrome mounts only after hydration inside the builder.
Metafield values are live entity data. The page builder never edits them.
Notes
- The component is presentational only — no fetching, no entity-context fallback.
- The declaration lookup is keyed
owner.namespace.fieldKey. An undeclared field still renders whatever the GraphQL data contains, on a best-effort basis. - Development-mode
console.warns cover the common misconfigurations: a declared field missing from the entity data (definition not provisioned yet, or the query does not selectmetafields), a stored/declared type mismatch, and a non-scalar value with norenderprop. They are silenced in production builds.
Related Components
- Area - Component container system
- ProductContext - Product page data
- CategoryContext - Category page data
- Editor - Rich-text renderer for
rich_textmetafields
For declaring metafields in theme.json, see Using Metafields in a Theme.
Support us
EverShop is an open-source project that relies on community support. If you find our project useful, please consider sponsoring us.