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.
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-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13Then 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.13Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/image-fieldIts 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.13import { 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
FileFieldinstead for other files or several files, andImageCropperalone 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
aspectandshape:croporcrop={{ 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), ornullwhen 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" circleshows and crops a round photo, for a profile;recta 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 sayscrop:<ImageField crop />.defaultValue: Not a prop. The photo shown issrc:<ImageField src={user.photoUrl} />.maxFileSize: Not a prop. Ouro UI saysmaxSize, in bytes:<ImageField maxSize={5 * 1024 * 1024} />.onChange: Not a prop. Ouro UI saysonFileChange:<ImageField onFileChange={setPhoto} />.value: Not a prop. The photo shown issrc:<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
- layout-classes: test:registry/ouro/components/ouro/image-field/image-field.test.tsx::ImageField > forwards a ref to the file input, as a function or an object, and lets layout classes win
verify:testExists(registry/ouro/components/ouro/image-field/image-field.test.tsx::ImageField > forwards a ref to the file input, as a function or an object, and lets layout classes win)