Nexus DSdocs

Attachment

report.pdf

2.4 MB

Installation

npm install @radix-ui/react-progress@^1.1.8 @radix-ui/react-separator@^1.1.8 @radix-ui/react-slot@^1.2.4 class-variance-authority@^0.7.1 clsx@^2.1.1 tailwind-merge@^3.4.0
components/item/index.ts
components/item/item.tsx
components/progress/index.ts
components/progress/progress.css
components/progress/progress.tsx
components/separator/index.ts
components/separator/separator.tsx
lib/utils.ts
components/attachment/attachment.tsx
components/attachment/index.ts
/* app/globals.css */
@import '../components/progress/progress.css';

The code below also uses:

npm install @tabler/icons-react@^3.36.1
components/button-group/button-group-context.ts
components/spinner/index.ts
components/spinner/spinner.tsx
lib/icons.ts
components/button/button.tsx
components/button/index.ts

Code

'use client';

import { IconFileTypePdf, IconX } from '@tabler/icons-react';

import {
  Attachment,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentMedia,
  AttachmentTitle,
} from '@/components/attachment/attachment';
import { Button } from '@/components/button/button';

export default function AttachmentDemo() {
  return (
    <Attachment className="nx:w-80">
      <AttachmentMedia variant="icon">
        <IconFileTypePdf />
      </AttachmentMedia>
      <AttachmentContent>
        <AttachmentTitle>report.pdf</AttachmentTitle>
        <AttachmentDescription>2.4 MB</AttachmentDescription>
      </AttachmentContent>
      <AttachmentActions>
        <Button variant="ghost" size="icon-sm" aria-label="Remove report.pdf">
          <IconX />
        </Button>
      </AttachmentActions>
    </Attachment>
  );
}

Props

Attachment

Attachment props
PropTypeDefaultDescription
orientation"horizontal" | "vertical" | null"horizontal"No description
size"default" | "sm" | null"default"No description
stateAttachmentState"done"

The attachment's lifecycle state. Drives the border treatment (dashed while idle, error-coloured while error) and the text announced to assistive tech.

statusLabelstringNo default documented

Overrides the assistive-tech status text for the current state — pass a localised string, or one that names the file. Ignored while the attachment is settled (idle / done), which announces nothing. Defaults to a built-in phrase per state, such as 'Uploading'.

AttachmentActions

AttachmentActions adds no props of its own; it accepts the props of the element or primitive it wraps.

AttachmentContent

AttachmentContent adds no props of its own; it accepts the props of the element or primitive it wraps.

AttachmentDescription

AttachmentDescription adds no props of its own; it accepts the props of the element or primitive it wraps.

AttachmentGroup

AttachmentGroup props
PropTypeDefaultDescription
aria-labelstringNo default documentedNo description
aria-labelledbystringNo default documentedNo description

AttachmentMedia

AttachmentMedia props
PropTypeDefaultDescription
variant"default" | "icon" | "image" | null"default"No description

AttachmentProgress

AttachmentProgress adds no props of its own; it accepts the props of the element or primitive it wraps.

AttachmentTitle

AttachmentTitle props
PropTypeDefaultDescription
trailingReactNodeNo default documented

A badge or status icon pinned after the file name. It keeps ItemTitle's gap-2 spacing and its own width while the name truncates beside it.

AttachmentTrigger

AttachmentTrigger props
PropTypeDefaultDescription
asChildbooleanfalse

Render as the child element via Radix Slot — e.g. an <a> opening the file — keeping the full-card hit area.

Examples

States

Drop a file here

PDF or PNG, up to 10 MB

Uploading
report.pdf

62% of 2.4 MB

Processing
scan.png

Processing…

Upload failed
archive.zip

Upload failed

notes.txt

8 KB

'use client';

import {
  IconAlertCircle,
  IconFileTypePdf,
  IconFileTypeTxt,
  IconRefresh,
  IconUpload,
  IconX,
} from '@tabler/icons-react';

import {
  Attachment,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentMedia,
  AttachmentProgress,
  AttachmentTitle,
} from '@/components/attachment/attachment';
import { Button } from '@/components/button/button';
import { Spinner } from '@/components/spinner/spinner';

export default function AttachmentStates() {
  return (
    <div className="nx:flex nx:w-80 nx:flex-col nx:gap-3">
      <Attachment state="idle">
        <AttachmentMedia variant="icon">
          <IconUpload />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Drop a file here</AttachmentTitle>
          <AttachmentDescription>PDF or PNG, up to 10 MB</AttachmentDescription>
        </AttachmentContent>
      </Attachment>
      <Attachment state="uploading">
        <AttachmentMedia variant="icon">
          <IconFileTypePdf />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>report.pdf</AttachmentTitle>
          <AttachmentDescription>62% of 2.4 MB</AttachmentDescription>
          <AttachmentProgress value={62} aria-label="Uploading report.pdf" />
        </AttachmentContent>
        <AttachmentActions>
          <Button
            variant="ghost"
            size="icon-sm"
            aria-label="Cancel upload of report.pdf"
          >
            <IconX />
          </Button>
        </AttachmentActions>
      </Attachment>
      <Attachment state="processing">
        <AttachmentMedia variant="icon">
          <Spinner aria-hidden="true" />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>scan.png</AttachmentTitle>
          <AttachmentDescription>Processing…</AttachmentDescription>
        </AttachmentContent>
      </Attachment>
      <Attachment state="error">
        <AttachmentMedia variant="icon">
          <IconAlertCircle />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>archive.zip</AttachmentTitle>
          <AttachmentDescription>Upload failed</AttachmentDescription>
        </AttachmentContent>
        <AttachmentActions>
          <Button
            variant="ghost"
            size="icon-sm"
            aria-label="Retry upload of archive.zip"
          >
            <IconRefresh />
          </Button>
        </AttachmentActions>
      </Attachment>
      <Attachment state="done">
        <AttachmentMedia variant="icon">
          <IconFileTypeTxt />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>notes.txt</AttachmentTitle>
          <AttachmentDescription>8 KB</AttachmentDescription>
        </AttachmentContent>
      </Attachment>
    </div>
  );
}

Sizes

report.pdf

Default · 2.4 MB

notes.txt

Small · 8 KB

'use client';

import { IconFileTypePdf, IconFileTypeTxt } from '@tabler/icons-react';

import {
  Attachment,
  AttachmentContent,
  AttachmentDescription,
  AttachmentMedia,
  AttachmentTitle,
} from '@/components/attachment/attachment';

export default function AttachmentSizes() {
  return (
    <div className="nx:flex nx:w-80 nx:flex-col nx:gap-3">
      <Attachment size="default">
        <AttachmentMedia variant="icon">
          <IconFileTypePdf />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>report.pdf</AttachmentTitle>
          <AttachmentDescription>Default · 2.4 MB</AttachmentDescription>
        </AttachmentContent>
      </Attachment>
      <Attachment size="sm">
        <AttachmentMedia variant="icon">
          <IconFileTypeTxt />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>notes.txt</AttachmentTitle>
          <AttachmentDescription>Small · 8 KB</AttachmentDescription>
        </AttachmentContent>
      </Attachment>
    </div>
  );
}

Group

cover.png

412 KB

report.pdf

2.4 MB

notes.txt

8 KB

archive.zip

18 MB

brief.docx

96 KB

'use client';

import {
  IconFileTypeDocx,
  IconFileTypePdf,
  IconFileTypePng,
  IconFileTypeTxt,
  IconFileTypeZip,
} from '@tabler/icons-react';

import {
  Attachment,
  AttachmentContent,
  AttachmentDescription,
  AttachmentGroup,
  AttachmentMedia,
  AttachmentTitle,
} from '@/components/attachment/attachment';

const files = [
  { name: 'cover.png', size: '412 KB', icon: <IconFileTypePng /> },
  { name: 'report.pdf', size: '2.4 MB', icon: <IconFileTypePdf /> },
  { name: 'notes.txt', size: '8 KB', icon: <IconFileTypeTxt /> },
  { name: 'archive.zip', size: '18 MB', icon: <IconFileTypeZip /> },
  { name: 'brief.docx', size: '96 KB', icon: <IconFileTypeDocx /> },
];

export default function AttachmentGroupDemo() {
  return (
    <AttachmentGroup aria-label="Attached files" className="nx:w-80">
      {files.map((file) => (
        <Attachment key={file.name} orientation="vertical">
          <AttachmentMedia variant="icon">{file.icon}</AttachmentMedia>
          <AttachmentContent>
            <AttachmentTitle>{file.name}</AttachmentTitle>
            <AttachmentDescription>{file.size}</AttachmentDescription>
          </AttachmentContent>
        </Attachment>
      ))}
    </AttachmentGroup>
  );
}