Forms

ImageField

A field for one photo, shown large: the photo already saved (src) or the one just chosen, with Choose photo (Change photo once one shows) and Remove. With crop, a chosen photo opens the cropper first, and the cropped image is what shows, what onFileChange gets and what the form submits. A file of the wrong type, over maxSize or that the browser cannot open as a photo is turned away, keeping what showed, with a message under the field that screen readers announce. The photo is in a real file input, so a plain form submits it.

Profile photoPNG or JPG, up to 5 MB.
or drop a photo here

In Spoonjoy

Spoonjoy, a recipe app, is built with Ouro HX (how).

In its source: app/components/recipe/RecipeBuilder.tsx · app/components/account/ProfilePhotoField.tsx

Install

Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):

npx shadcn@latest init --base base --preset nova --yes --no-monorepo
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13

Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):

npx shadcn@latest add ourostack/ouro-ui/image-field#v1.2.13

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/image-field

Its tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):

npx shadcn@latest add ourostack/ouro-ui/image-field#v1.2.13 ourostack/ouro-ui/image-field-tests#v1.2.13
import { ImageField } from "@/components/ouro/image-field/image-field";

Paths use shadcn's default @/ alias; the CLI rewrites them to yours.

Example

<ImageField label="Profile photo" name="photo" src={user.photoUrl} shape="circle" crop maxSize={5 * 1024 * 1024} onRemove={removePhoto} />

Usage

A photo dragged onto the field lights its frame and says where to drop it; dropped, it goes through the same checks, cropper and messages as one chosen with the button, which stays the way to choose from the keyboard or a screen reader.

Use instead when

  • Use FileField instead for other files or several files, and ImageCropper alone to crop an image you already have.

Props

Type: ImageFieldProps.

accept
Type: string
Default: "image/*"
The types it takes, as the file input's accept.
alt
Type: string
Default: "" (the label names it)
The photo's text alternative while it shows src.
aspect
Type: number
Default: 1
The preview's width divided by its height, and the crop's.
changeLabel
Type: string
Default: "Change photo"
The choose button's label while a photo shows.
chooseLabel
Type: string
Default: "Choose photo"
The choose button's label while no photo shows.
className
Type: string
Layout only: margins, width (mt-6). Appearance classes warn in development.
crop
Type: boolean | ImageFieldCrop
Opens the cropper on every chosen photo, so it fits aspect and shape: crop or crop={{ outputWidth: 512 }}.
description
Type: ReactNode
Supporting text under the label, such as the formats and size it takes.
disabled
Type: boolean
Turns the field off.
dropHint
Type: string
Default: "or drop a photo here"
The words beside the choose button that say a photo can be dropped on the field, shown while it is enabled. An empty string leaves them out.
dropLabel
Type: string
Default: "Drop the photo here"
What the photo's frame says while a file is dragged over the field.
error
Type: ReadonlyErrorMessages
What is wrong, shown under the field in the destructive tone.
hideLabel
Type: boolean
Keeps the label for screen readers only.
label (required)
Type: string
What the image is, above it and naming the field: "Profile photo".
maxSize
Type: number
The largest file it takes, in bytes.
name
Type: string
The form field name the chosen (and cropped) photo submits under.
onFileChange
Type: ((file: File | null) => void)
Called with the chosen photo (cropped, with crop), or null when it is removed. A photo comes here once the browser has opened it.
onRemove
Type: (() => void)
Shows Remove while the field shows src, and is called when the person removes it.
ref
Type: Ref<HTMLInputElement>
Reaches the file input.
rejectMessage
Type: ((reason: ImageFieldRejection, file: File) => string)
The message for a file turned away, for apps in other languages.
removeLabel
Type: string
Default: "Remove"
The remove button's label.
required
Type: boolean
Marks the field as required: a mark after the label. Check that a photo was chosen when the form submits.
shape
Type: "circle" | "rect"
Default: "rect"
circle shows and crops a round photo, for a profile; rect a rounded rectangle.
src
Type: string | null
The photo the field shows until another is chosen, such as the one already saved.

Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

  • cropper: Not a prop. Ouro UI says crop: <ImageField crop />.
  • defaultValue: Not a prop. The photo shown is src: <ImageField src={user.photoUrl} />.
  • maxFileSize: Not a prop. Ouro UI says maxSize, in bytes: <ImageField maxSize={5 * 1024 * 1024} />.
  • onChange: Not a prop. Ouro UI says onFileChange: <ImageField onFileChange={setPhoto} />.
  • value: Not a prop. The photo shown is src: <ImageField src={user.photoUrl} />.

className is for layout only

Pass margins, width and grid placement (mt-4, w-full, col-span-2). Your classes always win over Ouro UI's: ImageField merges className last with cn (tailwind-merge), so a class that conflicts with its own replaces it.layout-classes

Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.

Alignment classes (text-center) warn too: set alignment on a parent element.

Types

ErrorMessage

One error message: text, or a React element for a message that carries a link or other markup.

type ErrorMessage = string | ReactElement;

ImageFieldCrop

How ImageField crops a chosen photo.

interface ImageFieldCrop {
  /** The saved image's width in pixels. @default 1024 */
  outputWidth?: number | undefined;
  /** The saved image's format. @default "image/jpeg" */
  type?: "image/jpeg" | "image/png" | "image/webp" | undefined;
  /** The cropper's title. @default "Crop photo" */
  title?: string | undefined;
}

ImageFieldRejection

Why a chosen file was turned away: not a type accept takes, over maxSize, or a photo the browser cannot open (such as HEIC outside Safari).

type ImageFieldRejection = "type" | "size" | "unreadable";

ReadonlyErrorMessages

A read-only message view accepted by fields and Form; mutable input arrays remain valid.

type ReadonlyErrorMessages = ErrorMessage | readonly ErrorMessage[];

Claim sources