Skip to content
  1. Keyline UI
  2. Attachment
Keyline UIComponents
  • Get started
    • Installation
    • Theming
    • Modes
    • Configuration
    • Composition
  • Actions
    • Button
    • Button group
    • Split button
    • Toggle
    • Toggle group
    • Toolbar
    • Menu
    • Menubar
    • Context menu
  • Inputs
    • Field and form
    • Checkbox
    • Checkbox group
    • Radio group
    • Switch
    • Slider
    • Segmented
    • Select
    • Native select
    • Combobox
    • Multiselect
    • Number field
    • OTP field
    • Time field
    • Date picker and calendar
    • Date range
  • Data
    • Table
    • Chart
    • Stat and meter
    • Status and badge
    • Progress
    • Skeleton
    • Empty
    • Avatar
    • Kbd
    • Marker
  • Navigation
    • Tabs
    • Breadcrumb
    • Pagination
    • Nav
    • Navigation menu
    • Command menu
  • Overlays
    • Dialog
    • Alert dialog
    • Drawer
    • Sheet
    • Popover
    • Hover card
    • Tooltip
    • Toast
    • Notice
  • Layout
    • App shell
    • Sidebar
    • Panel
    • Resizable
    • Scroll area
    • Accordion
    • Collapsible
    • Carousel
    • Inspector
    • Item
    • Direction
  • Conversation
    • Message
    • Bubble
    • Message scroller
    • Attachment
    • Questionnaire
  • Blocks
    • Shipments

Attachment

A file as a row: a glyph or thumbnail, the name and size, and its actions as cells. While it uploads, progress fills along its foot; if it fails, the row says why and where it stopped, and Retry starts it again. Long names keep their extension in view.

Loading the live example…

"use client"

import { useEffect, useRef, useState } from "react"
import {
  Attachment,
  AttachmentAction,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentGlyph,
  AttachmentMedia,
  AttachmentTitle,
} from "@/components/keyline/ui/attachment"
import { Button } from "@/components/keyline/ui/button"

type File = { id: number; name: string; size: string; kind: "file" | "image"; state: "uploading" | "error" | "done"; progress: number; fails: boolean }

const NEXT = [
  { name: "invoice-DM-4107.pdf", size: "212 KB", kind: "file" as const, fails: false },
  { name: "loading-dock.jpg", size: "1.8 MB", kind: "image" as const, fails: true },
  { name: "q4-price-list.pdf", size: "96 KB", kind: "file" as const, fails: false },
]

export function InvoiceUploads() {
  const [files, setFiles] = useState<File[]>([{ id: 0, name: "signed-delivery-note.pdf", size: "88 KB", kind: "file", state: "done", progress: 100, fails: false }])
  const n = useRef(1)

  // a pretend upload: 20% a tick; the dock photo fails at 60% the first time
  useEffect(() => {
    if (!files.some((f) => f.state === "uploading")) return
    const t = setInterval(() => {
      setFiles((fs) =>
        fs.map((f) => {
          if (f.state !== "uploading") return f
          if (f.fails && f.progress >= 60) return { ...f, state: "error", fails: false }
          const p = Math.min(100, f.progress + 20)
          return { ...f, progress: p, state: p === 100 ? "done" : "uploading" }
        }),
      )
    }, 400)
    return () => clearInterval(t)
  }, [files])

  const attach = () => {
    const id = n.current++
    const next = NEXT[(id - 1) % NEXT.length]
    setFiles((fs) => [...fs, { id, ...next, state: "uploading", progress: 0 }])
  }

  return (
    <div className="grid max-w-[460px] gap-3">
      <ul aria-label="Attachments" className="m-0 grid list-none gap-2 p-0">
        {files.map((f) => (
          <li key={f.id}>
            <Attachment state={f.state} progress={f.progress}>
              <AttachmentMedia>
                <AttachmentGlyph name={f.kind} />
              </AttachmentMedia>
              <AttachmentContent>
                <AttachmentTitle>{f.name}</AttachmentTitle>
                <AttachmentDescription>{f.state === "error" ? "The connection dropped at 60%." : f.size}</AttachmentDescription>
              </AttachmentContent>
              <AttachmentActions>
                {f.state === "error" ? (
                  <AttachmentAction aria-label={`Retry ${f.name}`} onClick={() => setFiles((fs) => fs.map((x) => (x.id === f.id ? { ...x, state: "uploading", progress: 0 } : x)))}>
                    <AttachmentGlyph name="retry" />
                  </AttachmentAction>
                ) : null}
                <AttachmentAction aria-label={`Remove ${f.name}`} onClick={() => setFiles((fs) => fs.filter((x) => x.id !== f.id))}>
                  <AttachmentGlyph name="close" />
                </AttachmentAction>
              </AttachmentActions>
            </Attachment>
          </li>
        ))}
      </ul>
      <Button size="compact" className="justify-self-start" onClick={attach}>
        Attach a file
      </Button>
    </div>
  )
}

Installation

npx shadcn@latest add @keyline-ui/attachment

Run it where your components.json is. The first time, map the @keyline-ui registry: one command, in Installation.

Usage

import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentGlyph, AttachmentMedia, AttachmentTitle } from "@/components/keyline/ui/attachment"
<Attachment state="uploading" progress={40}>
  <AttachmentMedia><AttachmentGlyph name="file" /></AttachmentMedia>
  <AttachmentContent>
    <AttachmentTitle>invoice-DM-4107.pdf</AttachmentTitle>
    <AttachmentDescription>212 KB</AttachmentDescription>
  </AttachmentContent>
  <AttachmentActions>
    <AttachmentAction aria-label="Remove invoice-DM-4107.pdf"><AttachmentGlyph name="close" /></AttachmentAction>
  </AttachmentActions>
</Attachment>

Props

PropTypeDefaultDescription
state"idle" | "uploading" | "processing" | "error" | "done"Default "done"Where the file stands.
progressnumberDefault —0–100 while uploading; on error, where it stopped.
size"default" | "compact" | "tile"Default "default"compact for a composer, tile for a thumbnail with the name under it.
layout"control" | "cell"Default "control"cell inside a Bubble, sharing its keylines.
AttachmentActionbuttonDefault —A cell with a glyph. Needs an aria-label that names the file.
AttachmentTriggerbuttonDefault —Makes the whole row open the file.
AttachmentGlyph{ name: "file" | "image" | "close" | "retry" | "download" }Default —Keyline&apos;s glyphs for files and their actions.

Attach the dock photo in the example: it fails part way the first time, so you can see the error and Retry.

Sheet 71 of 73PreviousMessage scrollerNextQuestionnaire
On this page