COMPONENTS/INPUTS

Attachment.

Choose files, check the details.

In practice

Up to 5 MB per file · .txt,.pdf,image/*

    Local file selection with type and size checks, removal and onValueChange(File[]). Default limit 10 MB per file. No upload transport; validate files again on your server.

    Installation

    Install Coal once. Components and TypeScript declarations are included. React 19 and React DOM 19 are peer dependencies.

    Terminal · local package
    npm install http://localhost:3100/downloads/chlohal-coal-ui-0.5.0.tgz

    This installs the local package. It is not yet published on npm. See installation for stylesheet setup and integration.

    Usage

    example.tsx
    "use client";
    import * as React from "react";
    import "@chlohal/coal-ui/styles.css";
    import { Attachment } from "@chlohal/coal-ui";
    export default function Example() {
        return (<Attachment label="Project attachments" accept=".txt,.pdf,image/*" multiple maxSize={5 * 1024 * 1024}/>);
    }
    

    Source

    The original Coal implementation. Use the package export to integrate it.

    packages/react/src/attachment.tsx
    "use client";
    import * as React from "react";
    import { cn, useValue } from "./internal.js";
    export type AttachmentProps = {
      label?: string;
      accept?: string;
      multiple?: boolean;
      maxSize?: number;
      disabled?: boolean;
      value?: File[];
      onValueChange?: (files: File[]) => void;
      className?: string;
    };
    function accepts(file: File, accept?: string) {
      return (
        !accept ||
        accept.split(",").some((rule) => {
          const r = rule.trim().toLowerCase();
          return r.startsWith(".")
            ? file.name.toLowerCase().endsWith(r)
            : r.endsWith("/*")
              ? file.type.toLowerCase().startsWith(r.slice(0, -1))
              : file.type.toLowerCase() === r;
        })
      );
    }
    export function Attachment({
      label = "Attach files",
      accept,
      multiple = false,
      maxSize = 10 * 1024 * 1024,
      disabled,
      value,
      onValueChange,
      className,
    }: AttachmentProps) {
      const [files, set] = useValue(value, [], onValueChange);
      const [error, setError] = React.useState("");
      const id = React.useId();
      return (
        <div className={cn("coal-attachment", className)}>
          <label htmlFor={id}>{label}</label>
          <input
            id={id}
            type="file"
            accept={accept}
            multiple={multiple}
            disabled={disabled}
            aria-describedby={`${id}-hint ${id}-error`}
            onChange={(e) => {
              const chosen = Array.from(e.target.files ?? []);
              const invalid = chosen.filter(
                (f) => f.size > maxSize || !accepts(f, accept),
              );
              setError(
                invalid.length
                  ? `Rejected: ${invalid.map((f) => f.name).join(", ")}. Check file type and size.`
                  : "",
              );
              set(chosen.filter((f) => !invalid.includes(f)));
              e.target.value = "";
            }}
          />
          <p id={`${id}-hint`}>
            Up to {Math.round((maxSize / 1024 / 1024) * 10) / 10} MB per file
            {accept ? ` · ${accept}` : ""}
          </p>
          <p id={`${id}-error`} role="status">
            {error}
          </p>
          <ul>
            {files.map((file, i) => (
              <li key={`${file.name}-${i}`}>
                <span>{file.name}</span>
                <button
                  type="button"
                  disabled={disabled}
                  aria-label={`Remove ${file.name}`}
                  onClick={() => set(files.filter((_, j) => i !== j))}
                >
                  ×
                </button>
              </li>
            ))}
          </ul>
        </div>
      );
    }
    

    Accessibility checklist

    • Provide an accessible name for controls, including icon-only buttons.
    • Check keyboard navigation, visible focus and disabled states in your application.
    • Do not rely on color alone to communicate status.