Amplify Up

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

KindEditor in ComposerStored value
stringText inputstring
numberNumber inputnumber
boolToggleboolean
optionSelect (label shown, value stored)string
richtextRich text editorMarkdown string

Which component to use

ComponentField typeRenders
<Field>string, numbera <span> with the text
<RichText>markdown / rich textrendered 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

DoDon'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 codeexpect the SDK to supply default copy
<h1><Field … /></h1>look for an as prop on Field
value + name together for computed outputvalue 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.

MessageFix
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 valuesAdd 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 ComponentContextProviderWrap the component in renderComponent