Westy Design Systemv0.2.7
OverviewComponentsExamplesStorybook

Foundations

  • Typography
  • Colors
  • Spacing
  • Radius
  • Shadows
  • Icons

Components

  • Accordion
  • Alert
  • Alert Dialog
  • Aspect Ratio
  • Avatar
  • Badge
  • Breadcrumb
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Data Table
  • Date Picker
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • Field
  • Hover Card
  • Input
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Label
  • Menubar
  • Native Select
  • Navigation Menu
  • Pagination
  • Popover
  • Progress
  • Questionnaire
  • Radio Group
  • Resizable
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Skeleton
  • Slider
  • Sonner
  • Spinner
  • Switch
  • Table
  • Tabs
  • Textarea
  • Toggle
  • Toggle Group
  • Tooltip

Chat

  • Attachment
  • Bubble
  • Marker
  • Message
  • Message Scroller

Chat

Attachment

Displays a file or image attachment with media, metadata, upload state, and actions.

Playground

sales-dashboard.pdfPDF · 2.4 MB
—state
—size
—orientation
—variant
—AttachmentActions
—AttachmentTrigger
—dir
Usage
An attachment is the card a file gets once it enters a conversation — in a composer before sending, in a thread after. It is a bg-card row that sizes itself to its content, so give it w-full when it should fill the composer instead.
sales-dashboard.pdfPDF · 2.4 MB
import {
  Attachment,
  AttachmentAction,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentMedia,
  AttachmentTitle,
} from "@workspace/ui/components/attachment"

<Attachment>
  <AttachmentMedia>
    <FileTextIcon />
  </AttachmentMedia>
  <AttachmentContent>
    <AttachmentTitle>sales-dashboard.pdf</AttachmentTitle>
    <AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>
  </AttachmentContent>
  <AttachmentActions>
    <AttachmentAction aria-label="Remove sales-dashboard.pdf">
      <XIcon />
    </AttachmentAction>
  </AttachmentActions>
</Attachment>
Composition
Every part is optional and the root reacts to what is present: AttachmentContent switches the padding on, and dropping it leaves a bare media tile. Wrap several attachments in AttachmentGroup to lay them out as a scrollable row.
Attachment
├── AttachmentMedia
├── AttachmentContent
│   ├── AttachmentTitle
│   └── AttachmentDescription
├── AttachmentActions
│   └── AttachmentAction
└── AttachmentTrigger

AttachmentGroup
├── Attachment
└── Attachment
Image
Set variant="image" on AttachmentMedia and render an img inside it — the slot crops it square with object-cover and dims it until the state is done, so an upload in flight reads as unfinished without a second indicator.
A workspace seen from above
workspace.pngPNG · 820 KB
An open-plan desk
desk-reference.jpgJPG · 1.1 MB
An office interior
office-reference.jpgJPG · 940 KB
<AttachmentGroup>
  {images.map((image) => (
    <Attachment key={image.name} orientation="vertical">
      <AttachmentMedia variant="image">
        <img src={image.src} alt={image.alt} />
      </AttachmentMedia>
      <AttachmentContent>
        <AttachmentTitle>{image.name}</AttachmentTitle>
        <AttachmentDescription>{image.meta}</AttachmentDescription>
      </AttachmentContent>
      <AttachmentActions>
        <AttachmentAction aria-label={`Remove ${image.name}`}>
          <XIcon />
        </AttachmentAction>
      </AttachmentActions>
      <AttachmentTrigger asChild>
        <a
          href={image.src}
          target="_blank"
          rel="noreferrer"
          aria-label={`Open ${image.name}`}
        />
      </AttachmentTrigger>
    </Attachment>
  ))}
</AttachmentGroup>
States
state mirrors to data-state on the root, and every part styles itself from there. uploading and processing shimmer the title, idle dashes the border, and error turns the media tile and the description destructive.
selected-file.pdfReady to upload
design-system.zipUploading · 64%
market-research.pdfProcessing document
financial-model.xlsxUpload failed. Try again.
uploaded-report.pdfUploaded · 1.8 MB
<Attachment state="idle">…</Attachment>
<Attachment state="uploading">
  <AttachmentMedia>
    <Spinner />
  </AttachmentMedia>
  <AttachmentContent>
    <AttachmentTitle>design-system.zip</AttachmentTitle>
    <AttachmentDescription>Uploading · 64%</AttachmentDescription>
  </AttachmentContent>
</Attachment>
<Attachment state="processing">…</Attachment>
<Attachment state="error">…</Attachment>
<Attachment state="done">…</Attachment>
When to use each state
StateDescription
idlePicked but not sent yet. The dashed border says the file is still a draft.
uploadingBytes are moving. Pair the shimmer with a Spinner in the media slot and a percentage in the description.
processingUploaded, but the server is still working on it — parsing, transcoding, indexing.
errorThe upload failed. Keep the reason in the description and offer a retry action.
doneThe default. The file is settled, so the description carries its type and size.
Sizes
size scales the media tile, the padding, and the text together: default for a composer, sm for an attachment hanging off a message, xs for a dense list. At xs the description is usually one line too many — drop it.
Default attachmentPDF · 2.4 MB
Small attachmentPDF · 2.4 MB
Extra small attachment
<Attachment size="default">…</Attachment>
<Attachment size="sm">…</Attachment>
<Attachment size="xs">
  <AttachmentMedia>
    <FileTextIcon />
  </AttachmentMedia>
  <AttachmentContent>
    <AttachmentTitle>Extra small attachment</AttachmentTitle>
  </AttachmentContent>
</Attachment>
Orientation
vertical stacks the media above the content and fixes the card width, which is what turns an attachment into a thumbnail. The actions move into the top corner as an overlay, and a vertical attachment with no AttachmentContent collapses to the tile alone.
A workspace seen from above
workspace.pngPNG · 820 KB
<Attachment orientation="vertical">
  <AttachmentMedia variant="image">
    <img src={image.src} alt={image.alt} />
  </AttachmentMedia>
  <AttachmentContent>
    <AttachmentTitle>workspace.png</AttachmentTitle>
    <AttachmentDescription>PNG · 820 KB</AttachmentDescription>
  </AttachmentContent>
</Attachment>

<Attachment orientation="vertical">
  <AttachmentMedia>
    <FileCodeIcon />
  </AttachmentMedia>
</Attachment>
Group
AttachmentGroup is a horizontal scroller with snap points, hidden scrollbars, and a mask that fades the content at whichever edge still has more to show. Children are pinned to flex-none, so give each attachment its own width.
briefing-notes.pdfPDF · 1.4 MB
renderer.tsxTSX · 12 KB
customers.csvCSV · 18 KB
sales-dashboard.pdfPDF · 2.4 MB
<AttachmentGroup>
  {items.map((item) => (
    <Attachment key={item.name} className="w-64">
      <AttachmentMedia>
        <item.icon />
      </AttachmentMedia>
      <AttachmentContent>
        <AttachmentTitle>{item.name}</AttachmentTitle>
        <AttachmentDescription>{item.meta}</AttachmentDescription>
      </AttachmentContent>
    </Attachment>
  ))}
</AttachmentGroup>
Trigger
AttachmentTrigger is an absolute overlay that makes the whole card activate something — a preview dialog, a download, a link. It sits at z-10 and AttachmentActions at z-20, so the buttons stay clickable on top of it rather than fighting it for the click.
research-summary.pdfOpen preview dialog
<Dialog>
  <Attachment>
    {/* media, content, actions */}
    <DialogTrigger asChild>
      <AttachmentTrigger aria-label="Preview research-summary.pdf" />
    </DialogTrigger>
  </Attachment>
  <DialogContent>{/* … */}</DialogContent>
</Dialog>
In a conversation
Inside a thread an attachment is a sibling of the bubble, not content inside it — MessageContent stacks both and aligns them to the same edge. Use size="sm" there so the card reads as an annotation on the message rather than as a second message.
Can you pull the numbers out of this deck?
q3-review.pdfPDF · 3.1 MB
W
Here they are, split by region and quarter.
q3-numbers.csvCSV · 24 KB
<Message align="end">
  <MessageContent>
    <Bubble align="end">
      <BubbleContent>Can you pull the numbers out of this deck?</BubbleContent>
    </Bubble>
    <Attachment size="sm">
      <AttachmentMedia>
        <FileTextIcon />
      </AttachmentMedia>
      <AttachmentContent>
        <AttachmentTitle>q3-review.pdf</AttachmentTitle>
        <AttachmentDescription>PDF · 3.1 MB</AttachmentDescription>
      </AttachmentContent>
    </Attachment>
  </MessageContent>
</Message>
Accessibility
AttachmentAction renders a Button and is almost always icon-only, so every one needs an aria-label naming both the action and its target. AttachmentTrigger covers the card with no text of its own and needs a label for what activating it does; because it sits behind the actions in the stacking order, the two never trap each other and both stay separately focusable. A group of presentational attachments scrolls horizontally with nothing to tab to — give it tabIndex={0}, role="group", and an aria-label so a keyboard can reach the off-screen items. Finally, the error state is a color: keep the reason for the failure in AttachmentDescription so it is never carried by the color alone.
<AttachmentAction aria-label="Remove sales-dashboard.pdf">
  <XIcon />
</AttachmentAction>

<AttachmentTrigger asChild>
  <a
    href={url}
    target="_blank"
    rel="noreferrer"
    aria-label="Open workspace.png"
  />
</AttachmentTrigger>

<AttachmentGroup tabIndex={0} role="group" aria-label="Attachments">
  {/* presentational attachments */}
</AttachmentGroup>
API Reference
Nine parts, all plain div elements except AttachmentTitle and AttachmentDescription (span), AttachmentAction (a Button), and AttachmentTrigger (a button). All native props pass through.
PartPropDefault
Attachmentstate?: "idle" | "uploading" | "processing" | "error" | "done""done"
Attachmentsize?: "default" | "sm" | "xs""default"
Attachmentorientation?: "horizontal" | "vertical""horizontal"
AttachmentMediavariant?: "icon" | "image""icon"
AttachmentContentclassName?: string—
AttachmentTitleclassName?: string—
AttachmentDescriptionclassName?: string—
AttachmentActionsclassName?: string—
AttachmentAction...props: React.ComponentProps<typeof Button>size="icon-xs"
AttachmentTriggerasChild?: booleanfalse
AttachmentGroupclassName?: string—

Tokens used

15 design tokens consumed by the Attachment component.

  • --card

    Card

    bg-card
  • --card-foreground

    Card Foreground

    text-card-foreground
  • --muted

    Muted

    bg-mutedhas-[>a,>button]:hover:bg-muted/50
  • --foreground

    Foreground

    text-foreground
  • --muted-foreground

    Muted Foreground

    text-muted-foreground
  • --destructive

    Destructive

    data-[state=error]:border-destructive/30group-data-[state=error]/attachment:bg-destructive/10group-data-[state=error]/attachment:text-destructivegroup-data-[state=error]/attachment:text-destructive/80
  • --border

    Border

    borderdata-[state=idle]:border-dashed
  • --ring

    Ring

    focus-within:ring-1focus-within:ring-ring/50
  • --radius-xl

    Radius Extra Large

    rounded-xl
  • --radius-lg

    Radius Large

    rounded-lg
  • --radius-md

    Radius Medium

    group-data-[size=xs]/attachment:rounded-md
  • --text-sm

    Text Small (14px)

    text-sm
  • --text-xs

    Text Extra Small (12px)

    text-xs
  • --font-weight-medium

    Font Weight Medium (500)

    font-medium
  • --spacing

    Spacing unit (4px)

    gap-2gap-2.5gap-1.5px-2.5py-2p-2w-10w-24w-30min-w-40size-4mt-0.5top-3end-3