ImageAndFileDisplay

Display images and files with labels, descriptions, and optional remove button

Usage

ImageAndFileDisplay is a component for displaying images and files with labels, descriptions, and optional remove button. It supports both horizontal and vertical orientations and can automatically show file sizes for file types.

Input description

Type
Remove button position
Size
Radius
Orientation
import { ImageAndFileDisplay } from '@thinker-core/mantine-core';

function Demo() {
  return (
    <ImageAndFileDisplay type="image" label="Input label" description="Input description" />
  );
}

With label

ImageAndFileDisplay supports labels that can be positioned alongside the display. In horizontal orientation, the label appears to the right of the display. In vertical orientation, it appears below the display.

Description on the right

Description on the bottom

import { ImageAndFileDisplay } from '@thinker-core/mantine-core';

function Demo() {
  return (
    <Flex direction="row" gap="xl" justify="center">
      <ImageAndFileDisplay
        my="xs"
        label="Label on the right"
        description="Description on the right"
        src=""
        type="image"
      />
      <ImageAndFileDisplay
        my="xs"
        label="Label on the bottom"
        description="Description on the bottom"
        orientation="vertical"
        src=""
        type="image"
      />
    </Flex>
  );
}

Display only

ImageAndFileDisplay can be used without labels or descriptions for a minimal display.

import { Group, ImageAndFileDisplay } from '@thinker-core/mantine-core';

function Demo() {
  return (
    <Group gap="md" justify="center">
      <ImageAndFileDisplay
        src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-7.png"
        type="image"
        orientation="vertical"
      />
      <ImageAndFileDisplay
        src={new File(['testing'], 'test.pdf', { type: 'application/pdf' })}
        type="file"
        orientation="vertical"
      />
    </Group>
  );
}

Overlay

Add an overlay to the display using the overlay prop.

99+
import { ImageAndFileDisplay } from '@thinker-core/mantine-core';

function Demo() {
  return (
    <ImageAndFileDisplay
      src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-7.png"
      type="image"
      orientation="vertical"
      overlay="99+"
    />
  );
}

Fullwidth (horizontal only)

When fullWidth is set to true (default), the component takes the full width of its container in horizontal orientation.

This is a description that is really long and take up a lot of space

import {ImageAndFileDisplay } from '@thinker-core/mantine-core';

function Demo() {
  return (
    <ImageAndFileDisplay
      label="Perhaps some label that are really long and take up a lot of space"
      description="This is a description that is really long and take up a lot of space"
      src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-7.png"
      type="image"
      orientation="horizontal"
      fullWidth
      withRemoveButton
    />
  );
}

Vertical orientation

Set orientation="vertical" to stack the label and description below the display instead of beside it.

Description on the bottom

Description on the bottom

Description on the bottom

import { Group, ImageAndFileDisplay } from '@thinker-core/mantine-core';

function Demo() {
  return (
    <Group gap="md" justify="center">
      <ImageAndFileDisplay
        my="xs"
        label="Label on the bottom"
        description="Description on the bottom"
        src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-7.png"
        type="image"
        orientation="vertical"
        removeButtonPosition="top-right"
      />
      <ImageAndFileDisplay
        my="xs"
        label="Label on the bottom"
        description="Description on the bottom"
        src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-7.png"
        type="image"
        orientation="vertical"
        removeButtonPosition="top-right"
      />
      <ImageAndFileDisplay
        my="xs"
        label="Label on the bottom"
        description="Description on the bottom"
        src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-7.png"
        type="image"
        orientation="vertical"
        removeButtonPosition="top-right"
      />
    </Group>
  );
}

File format

ImageAndFileDisplay can display file icons for different file formats. You can customize the file icon by providing a custom getFileIcon function. Currently supports the following file formats:

  • pdf
  • xls
  • xlsx
  • doc
  • docx
  • txt
  • tiff
  • gif
import { SimpleGrid, ImageAndFileDisplay } from '@thinker-core/mantine-core';

function Demo() {
  return (
    <Group
      gap="xl"
      justify="center"
      wrap="wrap"
    >
      {data.map((item) => (
        <ImageAndFileDisplay
          key={item.name}
          src={new File(['testing'], item.name, { type: item.format })}
          type="file"
          orientation="vertical"
        />
      ))}
    </Group>
  );
}

Design pattern (vertical)

Common design pattern is to display a list of files in a vertical orientation with a limited number of files displayed at a time using FileButton and a Collapse component.

Description

7 B

7 B

7 B

6+

2 MB

import { useMemo, useState } from 'react';
import {
  Box,
  Button,
  Collapse,
  FileButton,
  Group,
  ImageAndFileDisplay,
  Input,
  Stack,
} from '@thinker-core/mantine-core';

type MockData = {
  name: string;
  format?: string;
  type: 'image' | 'file';
  src: string | File;
  description?: string;
};

function Demo() {
  const [files, setFiles] = useState<MockData[]>([]);
  const [opened, setOpened] = useState(false);
  const top4 = useMemo(() => files.slice(0, 4), [files]);
  const remaining = useMemo(() => files.slice(4), [files]);

  const handleFiles = (files: File[]) => {
    const newFiles: MockData[] = [];

    // simulate image upload if the file is an image
    files.forEach((file) => {
      if (file.type.startsWith('image/')) {
        // get the src
        const src = URL.createObjectURL(file);

        // replace the file with the src
        newFiles.push({
          name: file.name,
          type: 'image',
          src,
          description:
            file.size > 1024 * 1024
              ? (file.size / 1024 / 1024).toFixed(2) + ' MB'
              : (file.size / 1024).toFixed(2) + ' KB',
        });
        return;
      }

      // simulate
      newFiles.push({
        name: file.name,
        type: 'file',
        format: file.type,
        src: file,
      });
    });

    setFiles((prev) => [...prev, ...newFiles]);
  };

  return (
    <Stack p="2xl" bg="ocean.0" style={{ borderRadius: 'var(--mantine-radius-md)' }}>
      <Group justify="space-between" mb="2xl">
        <Box>
          <Input.Label size="sm" required>
            Label
          </Input.Label>
          <Input.Description size="sm">Description</Input.Description>
        </Box>
        <Group gap="md">
          <Button variant="subtle" color="cherry" size="xs" onClick={() => setFiles([])}>
            Clear All
          </Button>
          <FileButton accept="image/*" onChange={handleFiles} multiple>
            {(props) => (
              <Button {...props} size="xs">
                Upload
              </Button>
            )}
          </FileButton>
        </Group>
      </Group>

      <Group gap="2xl">
        {top4.map((file, index) => (
          <ImageAndFileDisplay
            key={file.name}
            label={file.name}
            description={file.description}
            src={file.src}
            type={file.type}
            orientation="vertical"
            withRemoveButton
            removeButtonPosition="top-right"
            overlay={index === 3 && !opened ? (files.length - top4.length) + '+' : undefined} // display remaining files count
            onClick={index === 3 && !opened ? () => setOpened(true) : undefined}
            onRemove={() => {
              setFiles(files.filter((f) => f.name !== file.name));
            }}
          />
        ))}
      </Group>
      <Collapse in={opened}>
        <Group gap="2xl" pt="2xl">
          {remaining.map((file) => (
            <ImageAndFileDisplay
              key={file.name}
              label={file.name}
              description={file.description}
              src={file.src}
              type={file.type}
              orientation="vertical"
              withRemoveButton
              removeButtonPosition="top-right"
              onRemove={() => {
                setFiles(files.filter((f) => f.name !== file.name));
              }}
            />
          ))}
        </Group>
        <Button mt="2xl" variant="outline" size="xs" onClick={() => setOpened(false)} fullWidth>
          See less
        </Button>
      </Collapse>
    </Stack>
  );
}

Design pattern (horizontal)

Another common design pattern is to display a list of files in a horizontal orientation with a limited number of files displayed at a time using FileButton and a Collapse component.

Description

Error

7 B

7 B

7 B

2 MB

import { useMemo, useState } from 'react';
import {
  Box,
  Button,
  Collapse,
  FileButton,
  Group,
  ImageAndFileDisplay,
  Input,
  Stack,
} from '@thinker-core/mantine-core';

type MockData = {
  name: string;
  format?: string;
  type: 'image' | 'file';
  src: string | File;
  description?: string;
};

function Demo() {
  const [files, setFiles] = useState<MockData[]>([]);
  const [opened, setOpened] = useState(false);
  const top4 = useMemo(() => files.slice(0, 4), [files]);
  const remaining = useMemo(() => files.slice(4), [files]);
  const isLengthGreaterThan4 = files.length > 4;

  const handleFiles = (files: File[]) => {
    const newFiles: MockData[] = [];

    // simulate image upload if the file is an image
    files.forEach((file) => {
      if (file.type.startsWith('image/')) {
        // get the src
        const src = URL.createObjectURL(file);

        // replace the file with the src
        newFiles.push({
          name: file.name,
          type: 'image',
          src,
          description:
            file.size > 1024 * 1024
              ? (file.size / 1024 / 1024).toFixed(2) + ' MB'
              : (file.size / 1024).toFixed(2) + ' KB',
        });
        return;
      }

      // simulate
      newFiles.push({
        name: file.name,
        type: 'file',
        format: file.type,
        src: file,
      });
    });

    setFiles((prev) => [...prev, ...newFiles]);
  };

  return (
    <Stack gap={0}>
      <Stack gap="md" mb="2xl">
        <Box>
          <Input.Label size="sm" required>
            Label
          </Input.Label>
          <Input.Description size="sm">Description</Input.Description>
        </Box>
        <Group gap="md">
          <FileButton accept="image/*" onChange={handleFiles} multiple>
            {(props) => (
              <Button {...props} size="xs" variant="outline">
                Upload
              </Button>
            )}
          </FileButton>
          <Button variant="subtle" color="cherry" size="xs" onClick={() => setFiles([])}>
            Clear All
          </Button>
        </Group>
        <Input.Error size="sm">Error</Input.Error>
      </Stack>

      <Stack gap="md">
        {top4.map((file) => (
          <ImageAndFileDisplay
            key={file.name}
            label={file.name}
            description={file.description}
            src={file.src}
            type={file.type}
            orientation="horizontal"
            withRemoveButton
            size="sm"
            onRemove={() => {
              setFiles(files.filter((f) => f.name !== file.name));
            }}
          />
        ))}
      </Stack>
      <Collapse in={opened}>
        <Stack gap="md" pt="md">
          {remaining.map((file) => (
            <ImageAndFileDisplay
              key={file.name}
              label={file.name}
              description={file.description}
              src={file.src}
              type={file.type}
              orientation="horizontal"
              withRemoveButton
              size="sm"
              onRemove={() => {
                setFiles(files.filter((f) => f.name !== file.name));
              }}
            />
          ))}
        </Stack>
      </Collapse>
      {(opened ? true : isLengthGreaterThan4) && (
        <Button variant="outline" size="xs" onClick={() => setOpened(!opened)} fullWidth mt="2xl">
          See {opened ? 'less' : 'more (' + (files.length - top4.length) + ')'}
        </Button>
      )}
    </Stack>
  );
}