Deck
Browse the catalog

Build / Molecules

ImageUpload

  • molecule
  • play-fn

An image field: what it is, what it must be, what is there now, and how to change it.

The preview is shown at the shape the image will actually be cropped to, because a square thumbnail of something that ships as a wide banner tells you nothing about what you just uploaded.

Built on a real <input type="file"> driven by a <label>, so the picker opens from the keyboard and the control is announced as a file input rather than a button that mysteriously opens a dialog. The input keeps its own accessible name even while the styled button is what you see.

Deck has no upload pipeline — this hands you the File and gets out of the way. Rendering the result is the caller's job.

import { ImageUpload } from '@meetcard/deck'

Example

<ImageUpload label="Profile photo" shape="circle" src={photo} description="Square, at least 400×400px. JPG or PNG." onFileSelect={setFile} />

Stories

Every preview below is the real component, rendered in this page from the same story Storybook runs. The width control resizes each preview's container; CSS media queries still follow the browser window, so breakpoint behaviour is worth checking in Storybook.

Nothing uploaded yet. The constraints are stated before the picker opens.

Empty
Source
const Empty = () => <Card style={{ maxWidth: 480 }}>
    <ImageUpload
        label="Profile photo"
        description="Square, at least 400×400px. JPG or PNG."
        shape="circle" />
</Card>;

With an image, previewed at the crop it ships at, with replace and remove.

With Image
Source
const WithImage = () => <Card style={{ maxWidth: 480 }}>
    <ImageUpload
        label="Profile photo"
        description="Square, at least 400×400px. JPG or PNG."
        shape="circle"
        src="https://images.unsplash.com/photo-1633332755192-727a05c4013d?w=256&h=256&fit=crop"
        onRemove={() => {}} />
</Card>;

The three crops, side by side. The preview wears the shape the image will actually be used at — a person's photo, a company mark, a wide banner.

Shapes
Source
const Shapes = () => (
  <Stack gap={24} style={{ maxWidth: 480 }}>
    <ImageUpload
      label="Profile photo"
      shape="circle"
      src={PHOTO}
      description="Square, at least 400×400px."
    />
    <ImageUpload
      label="Logo"
      shape="square"
      description="Full-colour mark for light backgrounds. Square PNG or SVG."
    />
    <ImageUpload
      label="Cover image"
      shape="cover"
      description="4:3 image (1600×1200 recommended)."
    />
  </Stack>
);

Choosing a file names it back, since a picker confirms nothing itself.

Reports The Chosen File
Source
const ReportsTheChosenFile = () => {
    const [name, setName] = useState<string | null>(null)

    return (
        <Card style={{ maxWidth: 480 }}>
            <ImageUpload
                label="Profile photo"
                description="Square, at least 400×400px. JPG or PNG."
                shape="circle"
                onFileSelect={(file) => setName(file?.name ?? null)} />
            <Text size="xs" style={{ marginTop: 12 }}>Caller received: {name ?? '—'}</Text>
        </Card>
    );
};

Disabled — the image is shown but cannot be changed here.

Disabled
Source
const Disabled = () => <Card style={{ maxWidth: 480 }}>
    <ImageUpload
        label="Profile photo"
        description="Square, at least 400×400px. JPG or PNG."
        shape="circle"
        src="https://images.unsplash.com/photo-1633332755192-727a05c4013d?w=256&h=256&fit=crop"
        disabled />
</Card>;

Props

PropTypeDefaultDescription
labelrequiredReactNode—What is being uploaded — "Profile photo", "Logo", "Cover image".
acceptstring'image/png,image/jpeg,image/webp,image/svg+xml'File types offered in the picker. Defaults to common web images.
actionLabelstring—The button's words. Defaults to "Replace" once there is an image.
classNamestring——
descriptionReactNode—The constraints, in the reader's terms — "Square, at least 400×400px. JPG or PNG." Say them before the upload, not in an error afterwards.
disabledboolean——
onFileSelect(file: File | null) => void—The chosen file, or null when the picker is dismissed.
onRemove() => void—Renders a remove control. Omit when the image is not optional.
shape'circle' | 'square' | 'cover''square'circle for a person, square for a logo, cover for a wide banner. The preview's shape is the clearest statement of how the image is cropped, so it is shown at the aspect it will actually be used.
srcstring—The current image, if there is one.

Open in Storybookfor controls, the a11y panel and the interaction runner.