Attachment
File chips with preview or icon, name, meta line and remove action. States ride data-state; removal is one line of author JS.
INSTALL
1pnpm dlx @sayagodev/mo add attachmentOr import the files from the package (no copy):
1import "@sayagodev/mo/css/attachment.css";EXAMPLES
Demo
workspace.png
PNG · 820 KB
desk-reference.jpg
JPG · 1.1 MB
office-reference.jpg
JPG · 940 KB
sales-dashboard.pdf
Uploading · 64%
message-renderer.tsx
TypeScript · 12 KB
1<div style="display:grid; gap:1rem; max-width:21rem;">2 <div data-attachment-group>3 <div data-attachment data-orientation="vertical">4 <div data-attachment-media data-variant="image">5 <img src="https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80" alt="Workspace" />6 </div>7 <div data-attachment-content>8 <span data-attachment-title>workspace.png</span>9 <small data-attachment-description>PNG · 820 KB</small>10 </div>11 <div data-attachment-actions>12 <button type="button" data-attachment-remove class="icon ghost xs" aria-label="Remove workspace.png">13 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>14 </button>15 </div>16 </div>17 <div data-attachment data-orientation="vertical">18 <div data-attachment-media data-variant="image">19 <img src="https://images.unsplash.com/photo-1497215728101-856f4ea42174?w=900&auto=format&fit=crop&q=80" alt="Desk" />20 </div>21 <div data-attachment-content>22 <span data-attachment-title>desk-reference.jpg</span>23 <small data-attachment-description>JPG · 1.1 MB</small>24 </div>25 <div data-attachment-actions>26 <button type="button" data-attachment-remove class="icon ghost xs" aria-label="Remove desk-reference.jpg">27 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>28 </button>29 </div>30 </div>31 <div data-attachment data-orientation="vertical">32 <div data-attachment-media data-variant="image">33 <img src="https://images.unsplash.com/photo-1497366811353-6870744d04b2?w=900&auto=format&fit=crop&q=80" alt="Office" />34 </div>35 <div data-attachment-content>36 <span data-attachment-title>office-reference.jpg</span>37 <small data-attachment-description>JPG · 940 KB</small>38 </div>39 <div data-attachment-actions>40 <button type="button" data-attachment-remove class="icon ghost xs" aria-label="Remove office-reference.jpg">41 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>42 </button>43 </div>44 </div>45 </div>46 47 <div data-attachment data-state="uploading" style="width:100%">48 <div data-attachment-media>49 <span aria-busy="true"></span>50 </div>51 <div data-attachment-content>52 <span data-attachment-title>sales-dashboard.pdf</span>53 <small data-attachment-description>Uploading · 64%</small>54 </div>55 <div data-attachment-actions>56 <button type="button" data-attachment-remove class="icon ghost xs" aria-label="Cancel upload">57 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>58 </button>59 </div>60 </div>61 62 <div data-attachment style="width:100%">63 <div data-attachment-media>64 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="m10 12.5-2 2 2 2"/><path d="m14 16.5 2-2-2-2"/></svg>65 </div>66 <div data-attachment-content>67 <span data-attachment-title>message-renderer.tsx</span>68 <small data-attachment-description>TypeScript · 12 KB</small>69 </div>70 <div data-attachment-actions>71 <button type="button" data-attachment-remove class="icon ghost xs" aria-label="Remove message-renderer.tsx">72 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>73 </button>74 </div>75 </div>76</div>77
Group
briefing-notes.pdf
PDF · 1.4 MB
workspace.png
PNG · 820 KB
customers.csv
CSV · 18 KB
renderer.tsx
TSX · 12 KB
1<div data-attachment-group style="max-width:22rem">2 <div data-attachment style="width:16rem">3 <div data-attachment-media>4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>5 </div>6 <div data-attachment-content>7 <span data-attachment-title>briefing-notes.pdf</span>8 <small data-attachment-description>PDF · 1.4 MB</small>9 </div>10 <div data-attachment-actions>11 <button type="button" data-attachment-remove class="icon ghost xs" aria-label="Remove briefing-notes.pdf">12 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>13 </button>14 </div>15 </div>16 <div data-attachment style="width:16rem">17 <div data-attachment-media data-variant="image">18 <img src="https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80" alt="" />19 </div>20 <div data-attachment-content>21 <span data-attachment-title>workspace.png</span>22 <small data-attachment-description>PNG · 820 KB</small>23 </div>24 <div data-attachment-actions>25 <button type="button" data-attachment-remove class="icon ghost xs" aria-label="Remove workspace.png">26 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>27 </button>28 </div>29 </div>30 <div data-attachment style="width:16rem">31 <div data-attachment-media>32 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/><path d="M12 3v18"/></svg>33 </div>34 <div data-attachment-content>35 <span data-attachment-title>customers.csv</span>36 <small data-attachment-description>CSV · 18 KB</small>37 </div>38 <div data-attachment-actions>39 <button type="button" data-attachment-remove class="icon ghost xs" aria-label="Remove customers.csv">40 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>41 </button>42 </div>43 </div>44 <div data-attachment style="width:16rem">45 <div data-attachment-media>46 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="m10 12.5-2 2 2 2"/><path d="m14 16.5 2-2-2-2"/></svg>47 </div>48 <div data-attachment-content>49 <span data-attachment-title>renderer.tsx</span>50 <small data-attachment-description>TSX · 12 KB</small>51 </div>52 <div data-attachment-actions>53 <button type="button" data-attachment-remove class="icon ghost xs" aria-label="Remove renderer.tsx">54 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>55 </button>56 </div>57 </div>58</div>59
Image
workspace.png
PNG · 820 KB
desk-reference.jpg
JPG · 1.1 MB
office-reference.jpg
JPG · 940 KB
1<div data-attachment-group style="max-width:22rem">2 <div data-attachment data-orientation="vertical">3 <div data-attachment-media data-variant="image">4 <img src="https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80" alt="Workspace" />5 </div>6 <div data-attachment-content>7 <span data-attachment-title>workspace.png</span>8 <small data-attachment-description>PNG · 820 KB</small>9 </div>10 <div data-attachment-actions>11 <button type="button" data-attachment-remove class="icon ghost xs" aria-label="Remove workspace.png">12 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>13 </button>14 </div>15 </div>16 <div data-attachment data-orientation="vertical">17 <div data-attachment-media data-variant="image">18 <img src="https://images.unsplash.com/photo-1497215728101-856f4ea42174?w=900&auto=format&fit=crop&q=80" alt="Desk" />19 </div>20 <div data-attachment-content>21 <span data-attachment-title>desk-reference.jpg</span>22 <small data-attachment-description>JPG · 1.1 MB</small>23 </div>24 <div data-attachment-actions>25 <button type="button" data-attachment-remove class="icon ghost xs" aria-label="Remove desk-reference.jpg">26 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>27 </button>28 </div>29 </div>30 <div data-attachment data-orientation="vertical">31 <div data-attachment-media data-variant="image">32 <img src="https://images.unsplash.com/photo-1497366811353-6870744d04b2?w=900&auto=format&fit=crop&q=80" alt="Office" />33 </div>34 <div data-attachment-content>35 <span data-attachment-title>office-reference.jpg</span>36 <small data-attachment-description>JPG · 940 KB</small>37 </div>38 <div data-attachment-actions>39 <button type="button" data-attachment-remove class="icon ghost xs" aria-label="Remove office-reference.jpg">40 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>41 </button>42 </div>43 </div>44</div>45
Sizes
Default attachment
PDF · 2.4 MB
Small attachment
PDF · 2.4 MB
Extra small attachment
1<div class="vstack gap-3" style="max-width:21rem">2 <div data-attachment style="width:100%">3 <div data-attachment-media>4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>5 </div>6 <div data-attachment-content>7 <span data-attachment-title>Default attachment</span>8 <small data-attachment-description>PDF · 2.4 MB</small>9 </div>10 </div>11 <div data-attachment data-size="sm" style="width:100%">12 <div data-attachment-media>13 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>14 </div>15 <div data-attachment-content>16 <span data-attachment-title>Small attachment</span>17 <small data-attachment-description>PDF · 2.4 MB</small>18 </div>19 </div>20 <div data-attachment data-size="xs" style="width:100%">21 <div data-attachment-media>22 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>23 </div>24 <div data-attachment-content>25 <span data-attachment-title>Extra small attachment</span>26 </div>27 </div>28</div>29
States
selected-file.pdf
Ready to upload
design-system.zip
Uploading · 64%
market-research.pdf
Processing document
financial-model.xlsx
Upload failed. Try again.
uploaded-report.pdf
Uploaded · 1.8 MB
1<div class="vstack gap-2" style="max-width:21rem">2 <div data-attachment data-state="idle" style="width:100%">3 <div data-attachment-media>4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>5 </div>6 <div data-attachment-content>7 <span data-attachment-title>selected-file.pdf</span>8 <small data-attachment-description>Ready to upload</small>9 </div>10 <div data-attachment-actions>11 <button type="button" data-attachment-remove class="icon ghost xs" aria-label="Remove selected-file.pdf">12 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>13 </button>14 </div>15 </div>16 17 <div data-attachment data-state="uploading" style="width:100%">18 <div data-attachment-media>19 <span aria-busy="true"></span>20 </div>21 <div data-attachment-content>22 <span data-attachment-title>design-system.zip</span>23 <small data-attachment-description>Uploading · 64%</small>24 </div>25 <div data-attachment-actions>26 <button type="button" data-attachment-remove class="icon ghost xs" aria-label="Cancel upload">27 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>28 </button>29 </div>30 </div>31 32 <div data-attachment data-state="processing" style="width:100%">33 <div data-attachment-media>34 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>35 </div>36 <div data-attachment-content>37 <span data-attachment-title>market-research.pdf</span>38 <small data-attachment-description>Processing document</small>39 </div>40 <div data-attachment-actions>41 <button type="button" data-attachment-remove class="icon ghost xs" aria-label="Remove market-research.pdf">42 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>43 </button>44 </div>45 </div>46 47 <div data-attachment data-state="error" style="width:100%">48 <div data-attachment-media>49 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>50 </div>51 <div data-attachment-content>52 <span data-attachment-title>financial-model.xlsx</span>53 <small data-attachment-description>Upload failed. Try again.</small>54 </div>55 <div data-attachment-actions>56 <button type="button" class="icon ghost xs" aria-label="Retry upload">57 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H5v3"/></svg>58 </button>59 <button type="button" data-attachment-remove class="icon ghost xs" aria-label="Remove financial-model.xlsx">60 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>61 </button>62 </div>63 </div>64 65 <div data-attachment data-state="done" style="width:100%">66 <div data-attachment-media>67 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>68 </div>69 <div data-attachment-content>70 <span data-attachment-title>uploaded-report.pdf</span>71 <small data-attachment-description>Uploaded · 1.8 MB</small>72 </div>73 <div data-attachment-actions>74 <button type="button" data-attachment-remove class="icon ghost xs" aria-label="Remove uploaded-report.pdf">75 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>76 </button>77 </div>78 </div>79</div>80
Trigger
label[data-attachment-trigger] wraps a hidden file input; removal from the list is one line of author JS (chip.remove()).
1<div class="vstack gap-2" style="max-width:21rem">2 <label data-attachment-trigger>3 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>4 Attach files5 <input type="file" multiple />6 </label>7 <small class="text-light">label[data-attachment-trigger] wraps a hidden file input; removal from the list is one line of author JS (chip.remove()).</small>8</div>9
USAGE
1<div data-attachment-group>2 <div data-attachment>3 <div data-attachment-media><svg … /></div>4 <div data-attachment-content>5 <span data-attachment-title>report.pdf</span>6 <small data-attachment-description>PDF · 2.4 MB</small>7 </div>8 <div data-attachment-actions>9 <button type="button" data-attachment-remove class="icon ghost xs" aria-label="Remove">✕</button>10 </div>11 </div>12</div>13 14<label data-attachment-trigger>Attach files<input type="file" multiple /></label>
API
NAMEKINDDESCRIPTION
[data-attachment]attributeChip row: bordered bg-card, hover tint when it holds controls
data-stateattributeidle (dashed) · uploading/processing (spinner) · error · done
aria-invalid="true"attributeAlias for data-state="error"
[data-attachment-media]attribute40px slot; data-variant="image" renders cover thumb
[data-attachment-title] / [data-attachment-description]attributeTruncating name + muted meta line
label[data-attachment-trigger]elementFile-picker label styled like button.outline
--mo-attachment-radius / -radius-xstokenChip and compact chip corner radius
--mo-attachment-media-size / -size-sm / -size-xstokenMedia slot footprint (40px default)
--mo-attachment-gap / -group-gaptokenChip padding and inter-chip spacing
--mo-attachment-width-vertical / -vertical-contenttokenVertical tile width and inner content width
FILES
src/css/attachment.cssInstall:
mo add attachmentTHEMING · PLAYGROUND
This is the exact component from the examples above, wearing a completely different theme. Edit the tokens and --mo-* hooks in the editor — autocomplete knows them all — and the preview obeys in real time. Your CSS always wins.
Global tokens
--accent--background--border--card--card-foreground--destructive--foreground--input--muted--muted-foreground--radius--ringComponent hooks
--mo-attachment-gap--mo-attachment-group-gap--mo-attachment-media-radius--mo-attachment-media-size--mo-attachment-media-size-sm--mo-attachment-media-size-xs--mo-attachment-radius--mo-attachment-radius-xs--mo-attachment-width-vertical--mo-attachment-width-vertical-content--mo-button-font-size--mo-button-height--mo-button-padding-x--mo-button-radiusPreview
Your CSS
Try it: type --mo- for the component hooks, or edit any value — the preview obeys in real time.