Components
Render and edit content with Field, RichText, Image, and Slot.
Register each component in Amplify Up with the same component_id your site registry uses. This page covers a single component's own fields. For arrays of records, see Lists and queries.
Fields
Amplify Up passes a fields object on your component props. Every entry is a field envelope, not a raw value:
fields.heading;
// → { value: 'Welcome', name: 'heading' }
fields.subheading; // never filled in
// → { value: null, name: 'subheading' }
value is the content. name is the path Composer saves to — the SDK reads it off the envelope, so you never pass name yourself.
The shape is identical on the live site, in a draft, and in Composer. There is no mode where you get a bare string, and no mode where an empty field is undefined.
import { Field } from '@amplifyup/sdk/react';
import type { Fields } from '@amplifyup/sdk/react';
export function Hero({ fields }: { fields: Fields<{ heading: string; description: string }> }) {
return (
<section>
<h1>
<Field field={fields.heading} />
</h1>
<p>{fields.description.value}</p>
</section>
);
}
Show and edit, or just display
// Editable — authors can click it in Composer
<Field field={fields.heading} />
// Display only — read .value
<title>{fields.heading.value}</title>
Use the component whenever the text is visible on the page. Use .value when it is not something an author can click: a <meta> tag, an aria-label, an href, or a condition in your own code.
Never render the envelope itself:
// ✗ — this is the field object, not the string
<h1>{fields.heading}</h1>
// ✓
<h1><Field field={fields.heading} /></h1>
// ✓
<h1>{fields.heading.value}</h1>
Write targets
One rule explains where an edit goes:
A field is editable if and only if it carries a write target, and the write target is set by whoever produced the data. Nothing downstream guesses.
- A field on your component (
fields.heading) has no write target of its own. It saves to the page. - A field on a row in a list (
post.title) carries the record it came from. It saves to that record, so the same edit shows up everywhere that record appears.
You never set or read the write target. It exists so the same <Field> works in both places. Lists and queries covers the cases where a row does not get one.
Empty fields
An empty field renders nothing on the live site — the element collapses. In Composer it stays clickable so authors can fill it in. The SDK never invents default copy.
If you want a default, own it in your component:
// ✓ your default, in your code
<p>{fields.subheading.value ?? 'Built for teams that ship.'}</p>;
// ✓ hide the component when the key field is empty
if (!fields.message.value) return null;
Computed values
If you compute what you display, pass value and name together so Composer still knows which field a click edits. This is the only time you write name.
// ✓ show a formatted price, edit the raw number
<Field value={formatPrice(fields.price.value)} name={fields.price.name} />
// ✗ — redundant when you already pass a field
<Field field={fields.price} name="price" />
// ✗ — value with nothing to bind to
<Field value={formatPrice(fields.price.value)} />
Settings field kinds
| Kind | Editor in Composer | Stored value |
|---|---|---|
string | Text input | string |
number | Number input | number |
bool | Toggle | boolean |
option | Select (label shown, value stored) | string |
richtext | Rich text editor | Markdown string |
Which component to use
| Component | Field type | Renders |
|---|---|---|
<Field> | string, number | a <span> with the text |
<RichText> | markdown / rich text | rendered HTML in a <div> (change with as) |
<Image> | image | <img> |
All three take field and read name from it. Extra props (className, loading) pass through to the rendered element.
<Field> renders an inline <span>, so you supply the semantic tag around it:
// ✓
<h1><Field field={fields.heading} /></h1>
// ✗ — Field has no `as` prop
<Field as="h1" field={fields.heading} />
Rich text
Use <RichText> for markdown fields. Values are Markdown so they stay portable. In Composer, authors edit with a visual toolbar — they never write Markdown by hand.
import { Field, RichText } from '@amplifyup/sdk/react';
import type { Fields } from '@amplifyup/sdk/react';
export function ContentSection({ fields }: { fields: Fields<{ title: string; content: string }> }) {
return (
<section>
<h2>
<Field field={fields.title} />
</h2>
<RichText field={fields.content} className="prose" />
</section>
);
}
Prefer <RichText> over <Field> for markdown. Field is for plain text. HTML strings still pass through when the value already looks like HTML.
<RichText> owns a block element — as picks which one:
// ✓ default is <div>
<RichText field={fields.body} className="prose" />
// ✓ pick a different block element
<RichText field={fields.body} as="article" />
// ✗ — don't nest a block element inside a paragraph
<p><RichText field={fields.body} /></p>
Images
Use <Image> for image fields. Values are { url, alt, width, height } from the content source — Amplify Up does not host files. In Composer, authors pick from the connected library, upload, or paste a link.
import { Image } from '@amplifyup/sdk/react';
import type { Fields, ImageValue } from '@amplifyup/sdk/react';
export function Hero({ fields }: { fields: Fields<{ backgroundImage: ImageValue }> }) {
return (
<section className="relative">
<Image field={fields.backgroundImage} className="absolute inset-0 h-full w-full object-cover" />
</section>
);
}
A plain URL string still works. <Field> will not render an image.
// ✓ editable, and authors get the picker
<Image field={fields.cover} />
// ✗ — renders, but authors cannot change it in Composer
<img src={fields.cover.value.url} />
If you compute the image, pass that result as value and the schema path as name together.
Slots
A slot is a region where authors drop other components.
import { Slot } from '@amplifyup/sdk/react';
export function Grid5050() {
return (
<div className="grid grid-cols-2 gap-6">
<Slot name="left" />
<Slot name="right" />
</div>
);
}
<Slot> takes only name and className. It reads its content from ComponentContextProvider, so you never thread a slots prop through your own component.
// ✗ — Slot has no `slots` prop
<Slot name="left" slots={slots} />
// ✓
<Slot name="left" />
Slot names must match the component's declared slots in Amplify Up. Slots hold components, not fields — nothing in a slot is read through fields.
Do / Don't
| Do | Don't |
|---|---|
<Field field={fields.heading} /> | <Field field={fields.heading} name="heading" /> |
{fields.heading.value} for non-visible uses | {fields.heading} as a JSX child |
{fields.x.value ?? 'default'} in your code | expect the SDK to supply default copy |
<h1><Field … /></h1> | look for an as prop on Field |
value + name together for computed output | value alone, or name alone |
<Image field={fields.cover} /> | <img src={fields.cover.value.url} /> when it should be editable |
<RichText field={fields.body} /> for markdown | <Field field={fields.body} /> for markdown |
<Slot name="left" /> | <Slot name="left" slots={slots} /> |
Console messages
In development the SDK says exactly what is wrong. Production is silent.
| Message | Fix |
|---|---|
Field expects a scalar field. Did you mean <Field field={post.title} />? | You passed a list or a whole row — pass one scalar field |
Field value={…} requires name="…" for computed values | Add name={fields.x.name} |
Field needs field={fields.yourProp} (or value + name for computed values) | Pass field= |
title has no write target; rendered read-only. | See Lists and queries |
Slot "left" used outside ComponentContextProvider | Wrap the component in renderComponent |