sdxc

Type to search, or start from one of these:

DropZone

A styled drop target for files.

Product photos

No photos queued

Whatever you drop in shows up here before it uploads.

View code
let session = new DragSession();
let queued: File[] = [];

function queueFiles(files: readonly File[]) {
	queued = [...queued, ...files];
	void handle.update();
}

<Header>Product photos</Header>
<DropZone
	name="photos"
	accept="image/png, image/jpeg, image/webp"
	multiple
	// The native input stays in the accessibility tree and the tab order; the
	// dashed label is what a person sees, and it draws the focus ring for both.
	parts={{ input: [visuallyHidden()] }}
	mix={[
		dropZone(session),
		on<HTMLLabelElement, "ui:drop-files">("ui:drop-files", (event) => queueFiles(event.files)),
		on<HTMLLabelElement, "change">("change", (event) => {
			let input = event.target as HTMLInputElement;
			queueFiles([...(input.files ?? [])]);
		}),
	]}
>
	<ImageIcon aria-hidden="true" />
	<span>Drag photos here, or click to browse</span>
	<span>PNG, JPEG or WebP · up to 10 MB each</span>
</DropZone>
{queued.length === 0 ? (
	<Empty>
		<Empty.Title>No photos queued</Empty.Title>
		<Empty.Description>Whatever you drop in shows up here before it uploads.</Empty.Description>
	</Empty>
) : (
	queued.map((file, index) => (
		<Item key={`${file.name}-${index}`}>
			<Item.Content>
				<Item.Title>{file.name}</Item.Title>
				<Item.Description>{formatFileSize(file.size)}</Item.Description>
			</Item.Content>
			<Item.Actions>
				<Button
					variant="ghost"
					color="neutral"
					aria-label={`Remove ${file.name}`}
					mix={[on<HTMLButtonElement, "click">("click", () => removeFile(index))]}
				>
					<XIcon />
				</Button>
			</Item.Actions>
		</Item>
	))
)}

Installation

An ordinary dependency: install it, import the theme once, and import the component where it is used.

npm add @sdxc/ui
import "@sdxc/ui/theme.css";

Usage

import { DropZone } from "@sdxc/ui";

Renders a native <input type="file"> wrapped in a <label> styled as a dashed, centered drop surface; a paired drag-and-drop mixin toggles the data-drop-target attribute this surface's styling reacts to.

Examples

<DropZone name="avatar" accept="image/png, image/jpeg" aria-label={t("profile.chooseAvatar")}>
	<ImageIcon aria-hidden />
</DropZone>
<DropZone name="documents" multiple accept="application/pdf" required>
	<p>{t("uploads.dropHint")}</p>
</DropZone>

Props

Read from the component's own types, so every prop, every default and every allowed value is listed.

PropTypeDescription
name?stringNative name submitted with an enclosing form.
accept?stringComma-separated list of accepted MIME types or file extensions.
multiple?booleanWhether more than one file may be chosen at once.
capture?TagProps<"input">["capture"]Requests a specific camera where a mobile browser's picker offers one.
required?booleanMarks the control required for its enclosing form.
disabled?booleanMarks the control inert and excluded from the tab order.
children?RemixNodeThe zone's visible content — an icon, an instructional caption, or both — rendered inside the <label> that wraps the file input, so clicking it opens the picker and names the input via label association.
parts?PartsPropsPer-part styling for this component's internally composed input.

Also accepts everything in TagProps<"label">.