suiss/ui
  • Docs
  • Components
  • Patterns
  • Interactions
  • Charts
  • Blocks
GitHubGet started
DocsComponentsPatternsInteractionsChartsBlocks
Components82
  • Overview
  • Accordion
  • Alert
  • Alert Dialog
  • Alert Sheet
  • Amount Input
  • Aspect Ratio
  • Attachment
  • Avatar
  • Badge
  • Bottom Bar
  • Breadcrumb
  • Bubble
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Chip
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • FAB
  • Field
  • Fullscreen Menu
  • Haptics
  • Header
  • Hover Card
  • Icon
  • Input
  • Input Accessory Bar
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Keyboard Scroll View
  • Keypad
  • Label
  • Marker
  • Menubar
  • Message
  • Message Scroller
  • Native Select
  • Navigation Menu
  • Page Indicator
  • Pagination
  • Pin Pad
  • Popover
  • Progress
  • QR Scanner
  • Questionnaire
  • Radio Group
  • Resizable
  • Screen
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Signature Pad
  • Skeleton
  • Slider
  • Spinner
  • Switch
  • Tab Bar
  • Table
  • Tabs
  • Text
  • Textarea
  • Toast
  • Toggle
  • Toggle Group
  • Tooltip
  • Wheel Picker
  • Widget

suiss/ui · Component

Attachment

A compact file card that shows a document or image with its name, details, upload state and actions.

Attachment is an open-source component from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

An Attachment typically contains an AttachmentMedia thumbnail, an AttachmentContent block with AttachmentTitle and AttachmentDescription, and an AttachmentActions slot holding one or more AttachmentAction buttons. AttachmentMedia can hold an icon, an img, or an AttachmentLabel file-type tag. During an upload, put an AttachmentProgress bar inside the content. Add an AttachmentTrigger to make the whole card clickable.

Wrap several attachments in an AttachmentGroup to render them as a single list with inset dividers. Use attachments for files in composers, chat messages and upload forms.

Installation

CLI

$ pnpm dlx shadcn@latest add @esuiss/attachment

Works in any project with a components.json. See Installation for the one-line registry setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add attachment

Manual

  1. Install the dependencies and add the cn helper:
$ pnpm add @base-ui/react class-variance-authority
  1. Add the pieces it builds on: Button.

  2. Copy the source into components/ui/attachment.tsx:

  1. Update the import paths to match your project.

Usage

import { Attachment, AttachmentLabel, AttachmentMedia, AttachmentContent, AttachmentTitle, AttachmentDescription, AttachmentProgress, AttachmentActions, AttachmentAction, AttachmentTrigger, AttachmentGroup } 
<Attachment












Set state to reflect the file's lifecycle. idle adds a dashed border (outside a group), and error colors the description with text-destructive. The state, size and orientation values are also exposed as data-* attributes for your own styling.

AttachmentAction renders a Button with the secondary variant and an icon size by default. Pass variant="link" for a text action such as "Try Again". Icon-only actions need an aria-label.

AttachmentTrigger is an invisible button stretched over the card. Give it an aria-label, or use render to turn it into a link. Actions sit above it, so they remain separately clickable.

Examples

Sizes

Compares the default, sm and xs sizes.

States

Shows the idle, uploading, processing, error and done states.

Drop zone

Image

Uses an image thumbnail with the image media variant.

Vertical

Stacks the media above the content with orientation="vertical".

Clickable

Makes the whole card a button with AttachmentTrigger, alongside a separate download action.

Group

Combines several attachments in one AttachmentGroup list.

File types

Uses colored AttachmentLabel tags to indicate file extensions.

File list

A full-width list of separate attachment cards.

Uploaded

Marks completed uploads with a check icon in the actions slot.

API Reference

Anatomy20 parts
<Attachment>
<AttachmentMedia>
<AttachmentLabel />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle />
<AttachmentDescription />
01

Attachment

The card container that sets size, orientation and state.

PropTypeDefaultDescription
size"default" | "sm" | "xs""default"Overall density: default, sm or xs.
orientation"horizontal" | "vertical""horizontal"horizontal places media beside the content; vertical stacks it above and overlays actions on the media.
state"idle" | "uploading" | "processing" | "error" | "done""done"Upload lifecycle state. idle adds a dashed border and error colors the description red.

Also accepts the props of React.ComponentProps<"div">.

02

AttachmentLabel

A small uppercase tag, usually the file extension, shown inside the media.

Also accepts the props of React.ComponentProps<"span">.

03

AttachmentMedia

The thumbnail area for an icon, document tag or image.

PropTypeDefaultDescription
variant"icon" | "document" | "image""icon"Thumbnail style: icon for a bordered icon tile, document for a page shape with a bottom tag, image for a cropped image.

Also accepts the props of React.ComponentProps<"div">.

04

AttachmentContent

Wraps the title, description and progress bar.

Also accepts the props of React.ComponentProps<"div">.

05

AttachmentTitle

The file name, truncated to one line.

Also accepts the props of React.ComponentProps<"span">.

06

AttachmentDescription

Secondary details such as file size or an error message.

Also accepts the props of React.ComponentProps<"span">.

07

AttachmentProgress

A progress bar with a value readout for uploads.

Also accepts the props of the primitive part it wraps.

08

AttachmentActions

The slot for action buttons or status indicators.

Also accepts the props of React.ComponentProps<"div">.

09

AttachmentAction

A compact button for actions such as remove, cancel or download.

Also accepts the props of React.ComponentProps<typeof Button>.

10

AttachmentTrigger

An invisible button covering the card to make it clickable.

Also accepts the props of useRender.ComponentProps<"button">.

11

AttachmentGroup

Stacks attachments into one rounded list with dividers.

Also accepts the props of React.ComponentProps<"div">.

Accessibility

  • AttachmentProgress wraps the progress primitive and exposes the progressbar role with aria-valuenow; give it an aria-label.
  • AttachmentTrigger renders a button with type="button" by default; give it an aria-label that names the file.
  • The trigger and each AttachmentAction are separate tab stops, in that order.
  • The card shows a focus ring when its trigger has keyboard focus.

Notes

  • Styling hooks: data-slot values such as attachment, attachment-media, attachment-content and attachment-action, plus data-state, data-size and data-orientation on the root and data-variant on the media.
  • Inside AttachmentGroup, attachments fill the width, lose their own rounding and get inset top dividers.
  • AttachmentLabel uses bg-primary by default; override it with a color class per file type.

Attachment is a rounded View that lays out an AttachmentMedia thumbnail, an AttachmentContent column and AttachmentActions. It comes in default, sm and xs sizes and in horizontal or vertical orientation; the vertical layout makes a tile with the actions floating over the media.

The state prop reflects the upload lifecycle: idle draws a dashed border for an empty drop slot, error turns the description red, and uploading, processing and done are passed to the parts through context. Put several attachments in an AttachmentGroup to show them as one list with hairlines between rows.

Installation

$ pnpm dlx shadcn@latest add @esuiss-rn/attachment

See Installation for the components.json setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add attachment
$ pnpm dlx @esuiss/ui@latest add attachment --platform swift
NextAvatarPreviousAspect Ratio

On this page

  1. 01Installation
    • CLI
    • Manual
  2. 02Usage
  3. 03Examples
    • Sizes
    • States
    • Drop zone
    • Image
    • Vertical
    • Clickable
    • Group
    • File types
    • File list
    • Uploaded
  4. 04API Reference
    • Attachment
    • AttachmentLabel
    • AttachmentMedia
    • AttachmentContent
    • AttachmentTitle
    • AttachmentDescription
    • AttachmentProgress
    • AttachmentActions
    • AttachmentAction
    • AttachmentTrigger
    • AttachmentGroup
  5. 05Accessibility
  6. 06Notes
  7. 07Installation
    • Manual
  8. 08Usage
  9. 09Examples
    • Sizes
    • States
    • Image
    • Vertical
    • Pressable
    • Group
    • File types
  10. 10API Reference
    • Attachment
    • AttachmentMedia
    • AttachmentLabel
    • AttachmentContent
    • AttachmentTitle
    • AttachmentDescription
    • AttachmentProgress
    • AttachmentActions
    • AttachmentAction
    • AttachmentTrigger
    • AttachmentGroup
  11. 11Accessibility
  12. 12Notes
from
"@/components/ui/attachment"
orientation
=
"horizontal"
state
=
"done"
>
<AttachmentMedia variant="document">
<AttachmentLabel className="bg-red-600">pdf</AttachmentLabel>
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>Quarterly report.pdf</AttachmentTitle>
<AttachmentDescription>2.4 MB</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label="Remove">
<XIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>
<
AttachmentProgress
/>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction />
</AttachmentActions>
<AttachmentTrigger />
</Attachment>
<AttachmentGroup>
<Attachment>
<AttachmentMedia>
<AttachmentLabel />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle />
<AttachmentProgress />
<AttachmentDescription />
</AttachmentContent>
<AttachmentActions>
<AttachmentAction />
</AttachmentActions>
</Attachment>
</AttachmentGroup>
$ pnpm dlx @esuiss/ui@latest add attachment --platform kotlin

The CLI also adds what it builds on: Button, Icon, Progress, Text.

Manual

  1. Install the dependencies:
npx expo install cn
  1. Add the pieces it builds on: Button, Icon, Progress, Text, and the primitives components/primitives/progress.tsx, components/primitives/types.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/attachment.tsx:

  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install cn
  1. Add the pieces it builds on: Button, Icon, Progress, Text, and the primitives components/primitives/progress.tsx, components/primitives/types.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/attachment.tsx:

  1. Update the import paths to match your project.
  1. Add the pieces it builds on: Button, Icon, Progress, Text, and the primitives components/primitives/progress.tsx, components/primitives/types.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/attachment.tsx:

  1. Update the import paths to match your project.
  1. Target iOS 17 or later.

  2. Copy the theme files every view uses:

swift/Theme/Theme.swift
import SwiftUI
import UIKit

struct RGBA: Sendable {
  let red: CGFloat
  let green: CGFloat
  let blue: CGFloat
  let alpha: CGFloat

  init(_ hex: UInt32, _ alpha: CGFloat = 1) {
    red = CGFloat((hex >> 16) & 0xFF) / 255
    green = CGFloat((hex >> 8) & 0xFF) / 255
    blue = CGFloat(hex & 0xFF) / 255
    self.alpha = alpha
  }
swift/Theme/Tokens.swift
import SwiftUI

public enum EsuissColor {
  public static let background = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))
  public static let foreground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let card = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let cardForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let popover = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let popoverForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let primary = Color.adaptive(light: .init(0x0071E3), dark: .init(0x2997FF))
  public static let primaryForeground = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0xFFFFFF))
  public static let secondary = Color.adaptive(light: .init(0x000000, 0.04), dark: .init(0xFFFFFF, 0.1))
  public static let secondaryForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let muted = Color.adaptive(light: .init(0xF5F5F7), dark: .init(0x161617))
  public static let mutedForeground = Color.adaptive(light: .init(0x6E6E73), dark: .init(0xA1A1A6))
  public static let accent = Color.adaptive(light: .init(0x000000, 0.06), dark: .init(0xFFFFFF, 0.1))
  1. Copy the source and every file it uses:
swift/Components/Button.swift
import SwiftUI

public struct VariantButtonStyle: ButtonStyle {
  public enum Variant: CaseIterable, Sendable {
    case `default`, outline, secondary, tinted, ghost, plain, destructive, link
  }

  public enum Size: CaseIterable, Sendable {
    case `default`, xs, sm, lg, xl, icon, iconXs, iconSm, iconLg, iconXl

    var height: CGFloat {
      switch self {
      case .xs, .iconXs: 24
      case .sm, .iconSm: 32
      case .default, .icon: 36
      case .lg, .iconLg: 44
swift/Components/ButtonGroup.swift
import SwiftUI

extension EnvironmentValues {
  @Entry var buttonGroup: Axis? = nil
  @Entry var buttonGroupSize: CGSize = .zero
}

enum ButtonGroupSpace: Hashable {
  case group
}

struct ButtonGroupFrame {
  let bounds: Anchor<CGRect>
  let outlined: Bool
}
swift/Components/TextVariant.swift
import SwiftUI

public enum TextVariant: CaseIterable, Sendable {
  case `default`, h1, h2, h3, h4, p, blockquote, code, lead, large, small, muted

  var style: TextStyle {
    switch self {
    case .h1: .xl3
    case .h2: .xl2
    case .h3: .xl
    case .h4, .lead, .large: .lg
    case .code, .small, .muted: .sm
    default: .base
    }
  }
  1. Add the files to your app target.
  1. Set minSdk to 24 or higher and add the dependencies:
build.gradle.kts
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
  1. Copy the theme files every view uses:
kotlin/theme/Theme.kt
package com.esuiss.ui.theme

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.shadow.Shadow as DropShadow
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
kotlin/theme/Tokens.kt
package com.esuiss.ui.theme

import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Immutable
data class Colors(
    val background: Color,
    val foreground: Color,
    val card: Color,
    val cardForeground: Color,
    val popover: Color,
    val popoverForeground: Color,
    val primary: Color,
    val primaryForeground: Color,
  1. Copy the source and every file it uses:
kotlin/components/Button.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
kotlin/components/ButtonGroup.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
kotlin/components/Content.kt
package com.esuiss.ui.components

import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

val LocalContentStyle = compositionLocalOf { TextStyle.Default }

val LocalContentColor = compositionLocalOf { Color.Unspecified }

val LocalIconSize = compositionLocalOf { 20.dp }
kotlin/components/Icon.kt
package com.esuiss.ui.components

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import com.esuiss.ui.theme.EsuissTheme

@Composable
fun Icon(
    imageVector: ImageVector,
  1. Update the com.esuiss.ui package names to match your project.

Usage

import { Attachment, AttachmentMedia, AttachmentLabel, AttachmentContent, AttachmentTitle, AttachmentDescription, AttachmentProgress, AttachmentActions, AttachmentAction, AttachmentTrigger, AttachmentGroup } from "@/components/ui/attachment"
<Attachment orientation="horizontal" state="done">
  <AttachmentMedia variant="document">
    <AttachmentLabel className="bg-red-500">pdf</AttachmentLabel>
  </AttachmentMedia>
  <AttachmentContent>
    <AttachmentTitle>Quarterly report.pdf</AttachmentTitle>
    <AttachmentDescription>2.4 MB</AttachmentDescription>
  </AttachmentContent>
  <AttachmentActions>
    <AttachmentAction accessibilityLabel="Remove">
      <Icon as={XIcon} />
    </AttachmentAction>
  </AttachmentActions>
</Attachment>

AttachmentMedia has three variants: icon for a glyph, document for a page shape (pair it with AttachmentLabel to show a file extension), and image for a thumbnail. Put AttachmentTitle and AttachmentDescription in AttachmentContent, and add AttachmentProgress with a value while uploading.

AttachmentAction is a Button that defaults to the secondary variant and a small icon size scaled to the attachment, with an enlarged hit area. Use variant="link" for a text action.

To make the whole card pressable, add an AttachmentTrigger; it covers the card while actions stay pressable above it.

Examples

Sizes

The default, sm and xs sizes.

Web

Web preview

import { FileTextIcon, XIcon } from "phosphor-react-native"
import { View } from "react-native"
import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentMedia, AttachmentTitle } from "@/components/ui/attachment"
import { Icon } from "@/components/ui/icon"

export function AttachmentSizesExample() {
  return (
    <View className="items-start gap-3">
      {(["default", "sm", "xs"] as const).map((size) => (
        <Attachment key={size} orientation="horizontal" state="done" size={size}>
          <AttachmentMedia>
            <Icon as={FileTextIcon} />
          </AttachmentMedia>
          <AttachmentContent>
            <AttachmentTitle>Meeting notes.docx</AttachmentTitle>
            <AttachmentDescription>184 KB</AttachmentDescription>
          </AttachmentContent>
          <AttachmentActions>
            <AttachmentAction accessibilityLabel="Remove">
              <Icon as={XIcon} />
            </AttachmentAction>
          </AttachmentActions>
        </Attachment>
      ))}
    </View>
  )
}
import EsuissUI
import PhosphorSwift
import SwiftUI

struct AttachmentSizesExample: View {
  var body: some View {
    VStack(alignment: .leading, spacing: 12) {
      ForEach(AttachmentSize.allCases, id: \.self) { size in
        Attachment(size: size) {
          AttachmentMedia(icon: Ph.fileText.regular)
          AttachmentContent {
            AttachmentTitle("Meeting notes.docx")
            AttachmentDescription("184 KB")
          }
          AttachmentActions {
            AttachmentAction("Remove", icon: Ph.x.bold) {}
          }
        }
      }
    }
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.FileText
import com.adamglin.phosphoricons.regular.X
import com.esuiss.ui.components.Attachment
import com.esuiss.ui.components.AttachmentAction
import com.esuiss.ui.components.AttachmentActions
import com.esuiss.ui.components.AttachmentContent
import com.esuiss.ui.components.AttachmentDescription
import com.esuiss.ui.components.AttachmentMedia
import com.esuiss.ui.components.AttachmentSize
import com.esuiss.ui.components.AttachmentTitle
import com.esuiss.ui.components.Icon

@Composable
fun AttachmentIconAction(label: String, icon: androidx.compose.ui.graphics.vector.ImageVector = PhosphorIcons.Regular.X) {
    AttachmentAction(onClick = {}, contentDescription = label) {
        Icon(icon, contentDescription = null)
    }
}

@Composable
fun AttachmentSizes() {
    Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
        AttachmentSize.entries.forEach { size ->
            Attachment(size = size) {
                AttachmentMedia { Icon(PhosphorIcons.Regular.FileText, contentDescription = null) }
                AttachmentContent {
                    AttachmentTitle("Meeting notes.docx")
                    AttachmentDescription("184 KB")
                }
                AttachmentActions { AttachmentIconAction("Remove") }
            }
        }
    }
}

States

The idle, uploading, processing, error and done states.

Web

Web preview

import { UploadSimpleIcon, XIcon } from "phosphor-react-native"
import { View } from "react-native"
import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentLabel, AttachmentMedia, AttachmentProgress, AttachmentTitle } from "@/components/ui/attachment"
import { Icon } from "@/components/ui/icon"
import { Spinner } from "@/components/ui/spinner"

export function AttachmentStatesExample() {
  return (
    <View className="gap-3">
      <Attachment orientation="horizontal" state="idle" className="w-full">
        <AttachmentMedia>
          <Icon as={UploadSimpleIcon} />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Add a file</AttachmentTitle>
          <AttachmentDescription>
            PDF, PNG or JPG up to 10 MB
          </AttachmentDescription>
        </AttachmentContent>
        <AttachmentActions>
          <AttachmentAction variant="link">Choose File</AttachmentAction>
        </AttachmentActions>
      </Attachment>
      <Attachment orientation="horizontal" state="uploading" className="w-full">
        <AttachmentMedia variant="document">
          <AttachmentLabel className="bg-red-500">pdf</AttachmentLabel>
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Invoice March.pdf</AttachmentTitle>
          <AttachmentProgress value={64} accessibilityLabel="Uploading" />
        </AttachmentContent>
        <AttachmentActions>
          <AttachmentAction accessibilityLabel="Cancel upload">
            <Icon as={XIcon} />
          </AttachmentAction>
        </AttachmentActions>
      </Attachment>
      <Attachment orientation="horizontal" state="processing" className="w-full">
        <AttachmentMedia variant="document">
          <AttachmentLabel className="bg-red-500">pdf</AttachmentLabel>
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Invoice March.pdf</AttachmentTitle>
          <AttachmentDescription>Processing…</AttachmentDescription>
        </AttachmentContent>
        <AttachmentActions>
          <Spinner size={20} />
        </AttachmentActions>
      </Attachment>
      <Attachment orientation="horizontal" state="error" className="w-full">
        <AttachmentMedia variant="document">
          <AttachmentLabel className="bg-orange-500">key</AttachmentLabel>
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Presentation.key</AttachmentTitle>
          <AttachmentDescription>File type not supported</AttachmentDescription>
        </AttachmentContent>
        <AttachmentActions>
          <AttachmentAction accessibilityLabel="Remove">
            <Icon as={XIcon} />
          </AttachmentAction>
        </AttachmentActions>
      </Attachment>
      <Attachment orientation="horizontal" state="done" className="w-full">
        <AttachmentMedia variant="document">
          <AttachmentLabel className="bg-red-500">pdf</AttachmentLabel>
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Invoice March.pdf</AttachmentTitle>
          <AttachmentDescription>1.1 MB</AttachmentDescription>
        </AttachmentContent>
      </Attachment>
    </View>
  )
}
import EsuissUI
import PhosphorSwift
import SwiftUI

struct AttachmentStatesExample: View {
  var body: some View {
    VStack(spacing: 12) {
      Attachment(state: .idle, block: true) {
        AttachmentMedia(icon: Ph.uploadSimple.regular)
        AttachmentContent {
          AttachmentTitle("Add a file")
          AttachmentDescription("PDF, PNG or JPG up to 10 MB")
        }
        AttachmentActions {
          AttachmentAction("Choose File") {}
        }
      }
      Attachment(state: .uploading, block: true) {
        AttachmentMedia(.document) { AttachmentLabel("pdf", tint: EsuissColor.destructive) }
        AttachmentContent {
          AttachmentTitle("Invoice March.pdf")
          AttachmentProgress(value: 64, label: "Uploading")
        }
        AttachmentActions {
          AttachmentAction("Cancel upload", icon: Ph.x.bold) {}
        }
      }
      Attachment(state: .processing, block: true) {
        AttachmentMedia(.document) { AttachmentLabel("pdf", tint: EsuissColor.destructive) }
        AttachmentContent {
          AttachmentTitle("Invoice March.pdf")
          AttachmentDescription("Processing…")
        }
        AttachmentActions {
          Spinner(dimension: 20)
        }
      }
      Attachment(state: .error, block: true) {
        AttachmentMedia(.document) { AttachmentLabel("key", tint: Palette.amber500) }
        AttachmentContent {
          AttachmentTitle("Presentation.key")
          AttachmentDescription("File type not supported")
        }
        AttachmentActions {
          AttachmentAction("Remove", icon: Ph.x.bold) {}
        }
      }
      Attachment(state: .done, block: true) {
        AttachmentMedia(.document) { AttachmentLabel("pdf", tint: EsuissColor.destructive) }
        AttachmentContent {
          AttachmentTitle("Invoice March.pdf")
          AttachmentDescription("1.1 MB")
        }
      }
    }
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.UploadSimple
import com.adamglin.phosphoricons.regular.X
import com.esuiss.ui.components.Attachment
import com.esuiss.ui.components.AttachmentAction
import com.esuiss.ui.components.AttachmentActions
import com.esuiss.ui.components.AttachmentContent
import com.esuiss.ui.components.AttachmentDescription
import com.esuiss.ui.components.AttachmentLabel
import com.esuiss.ui.components.AttachmentMedia
import com.esuiss.ui.components.AttachmentMediaVariant
import com.esuiss.ui.components.AttachmentProgress
import com.esuiss.ui.components.AttachmentState
import com.esuiss.ui.components.AttachmentTitle
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Spinner
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Palette

@Composable
fun AttachmentDocument(extension: String, tone: Color) {
    AttachmentMedia(variant = AttachmentMediaVariant.Document) {
        AttachmentLabel(extension, background = tone)
    }
}

@Composable
fun AttachmentIconAction(label: String, icon: androidx.compose.ui.graphics.vector.ImageVector = PhosphorIcons.Regular.X) {
    AttachmentAction(onClick = {}, contentDescription = label) {
        Icon(icon, contentDescription = null)
    }
}

@Composable
fun AttachmentStates() {
    val colors = EsuissTheme.colors
    val fill = Modifier.fillMaxWidth()
    Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
        Attachment(fill, state = AttachmentState.Idle) {
            AttachmentMedia { Icon(PhosphorIcons.Regular.UploadSimple, contentDescription = null) }
            AttachmentContent {
                AttachmentTitle("Add a file")
                AttachmentDescription("PDF, PNG or JPG up to 10 MB")
            }
            AttachmentActions { AttachmentAction("Choose File", onClick = {}) }
        }
        Attachment(fill, state = AttachmentState.Uploading) {
            AttachmentDocument("pdf", Palette.red500)
            AttachmentContent {
                AttachmentTitle("Invoice March.pdf")
                AttachmentProgress(64f, contentDescription = "Uploading")
            }
            AttachmentActions { AttachmentIconAction("Cancel upload") }
        }
        Attachment(fill, state = AttachmentState.Processing) {
            AttachmentDocument("pdf", Palette.red500)
            AttachmentContent {
                AttachmentTitle("Invoice March.pdf")
                AttachmentDescription("Processing…")
            }
            AttachmentActions { Spinner(size = 20.dp) }
        }
        Attachment(fill, state = AttachmentState.Error) {
            AttachmentDocument("key", Palette.orange500)
            AttachmentContent {
                AttachmentTitle("Presentation.key")
                AttachmentDescription("File type not supported")
            }
            AttachmentActions { AttachmentIconAction("Remove") }
        }
        Attachment(fill, state = AttachmentState.Done) {
            AttachmentDocument("pdf", Palette.red500)
            AttachmentContent {
                AttachmentTitle("Invoice March.pdf")
                AttachmentDescription("1.1 MB")
            }
        }
    }
}

Image

An image thumbnail with the image media variant.

Web

Web preview

import { XIcon } from "phosphor-react-native"
import { Image as NativeImage } from "react-native"
import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentMedia, AttachmentTitle } from "@/components/ui/attachment"
import { Icon } from "@/components/ui/icon"

const imageUrl =
"https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&q=80"

function Thumbnail() {
  return (
    <NativeImage
      source={{ uri: imageUrl }}
      accessibilityLabel="Mountain lake"
      className="size-full"
      resizeMode="cover"
    />
  )
}

export function AttachmentImageExample() {
  return (
    <Attachment orientation="horizontal" state="done">
      <AttachmentMedia variant="image">
        <Thumbnail />
      </AttachmentMedia>
      <AttachmentContent>
        <AttachmentTitle>Lake.jpg</AttachmentTitle>
        <AttachmentDescription>3.2 MB</AttachmentDescription>
      </AttachmentContent>
      <AttachmentActions>
        <AttachmentAction accessibilityLabel="Remove">
          <Icon as={XIcon} />
        </AttachmentAction>
      </AttachmentActions>
    </Attachment>
  )
}
import EsuissUI
import PhosphorSwift
import SwiftUI

struct AttachmentImageExample: View {
  var body: some View {
    Attachment {
      AttachmentThumbnail()
      AttachmentContent {
        AttachmentTitle("Lake.jpg")
        AttachmentDescription("3.2 MB")
      }
      AttachmentActions {
        AttachmentAction("Remove", icon: Ph.x.bold) {}
      }
    }
  }
}

struct AttachmentThumbnail: View {
  var body: some View {
    AttachmentMedia(url: URL(string: "https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&q=80"))
      .accessibilityLabel(Text("Mountain lake"))
  }
}
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.painter.BrushPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.semantics.contentDescription
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.X
import com.esuiss.ui.components.Attachment
import com.esuiss.ui.components.AttachmentAction
import com.esuiss.ui.components.AttachmentActions
import com.esuiss.ui.components.AttachmentContent
import com.esuiss.ui.components.AttachmentDescription
import com.esuiss.ui.components.AttachmentMedia
import com.esuiss.ui.components.AttachmentTitle
import com.esuiss.ui.components.Icon
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Palette

@Composable
fun attachmentPhoto(): Painter {
    val colors = EsuissTheme.colors
    return remember(colors) { BrushPainter(Brush.linearGradient(listOf(Palette.teal500, Palette.blue500, Palette.indigo500))) }
}

@Composable
fun AttachmentIconAction(label: String, icon: androidx.compose.ui.graphics.vector.ImageVector = PhosphorIcons.Regular.X) {
    AttachmentAction(onClick = {}, contentDescription = label) {
        Icon(icon, contentDescription = null)
    }
}

@Composable
fun AttachmentImage() {
    val photo = attachmentPhoto()
    Row {
        Attachment {
            AttachmentMedia(photo, contentDescription = "Mountain lake")
            AttachmentContent {
                AttachmentTitle("Lake.jpg")
                AttachmentDescription("3.2 MB")
            }
            AttachmentActions { AttachmentIconAction("Remove") }
        }
    }
}

Vertical

Tiles with orientation="vertical".

Web

Web preview

import { FilePdfIcon, XIcon } from "phosphor-react-native"
import { Image as NativeImage, View } from "react-native"
import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentMedia, AttachmentTitle } from "@/components/ui/attachment"
import { Icon } from "@/components/ui/icon"

const imageUrl =
"https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&q=80"

function Thumbnail() {
  return (
    <NativeImage
      source={{ uri: imageUrl }}
      accessibilityLabel="Mountain lake"
      className="size-full"
      resizeMode="cover"
    />
  )
}

export function AttachmentVerticalExample() {
  return (
    <View className="flex-row items-start gap-3">
      <Attachment orientation="vertical" state="done">
        <AttachmentMedia variant="image">
          <Thumbnail />
        </AttachmentMedia>
        <AttachmentActions>
          <AttachmentAction accessibilityLabel="Remove">
            <Icon as={XIcon} />
          </AttachmentAction>
        </AttachmentActions>
      </Attachment>
      <Attachment orientation="vertical" state="done">
        <AttachmentMedia>
          <Icon as={FilePdfIcon} />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Contract.pdf</AttachmentTitle>
          <AttachmentDescription>860 KB</AttachmentDescription>
        </AttachmentContent>
      </Attachment>
    </View>
  )
}
import EsuissUI
import PhosphorSwift
import SwiftUI

struct AttachmentVerticalExample: View {
  var body: some View {
    HStack(alignment: .top, spacing: 12) {
      Attachment(orientation: .vertical) {
        AttachmentThumbnail()
        AttachmentActions {
          AttachmentAction("Remove", icon: Ph.x.bold) {}
        }
      }
      Attachment(orientation: .vertical) {
        AttachmentMedia(icon: Ph.filePdf.regular)
        AttachmentContent {
          AttachmentTitle("Contract.pdf")
          AttachmentDescription("860 KB")
        }
      }
    }
  }
}

struct AttachmentThumbnail: View {
  var body: some View {
    AttachmentMedia(url: URL(string: "https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&q=80"))
      .accessibilityLabel(Text("Mountain lake"))
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.painter.BrushPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.FilePdf
import com.adamglin.phosphoricons.regular.X
import com.esuiss.ui.components.Attachment
import com.esuiss.ui.components.AttachmentAction
import com.esuiss.ui.components.AttachmentActions
import com.esuiss.ui.components.AttachmentContent
import com.esuiss.ui.components.AttachmentDescription
import com.esuiss.ui.components.AttachmentMedia
import com.esuiss.ui.components.AttachmentOrientation
import com.esuiss.ui.components.AttachmentTitle
import com.esuiss.ui.components.Icon
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Palette

@Composable
fun attachmentPhoto(): Painter {
    val colors = EsuissTheme.colors
    return remember(colors) { BrushPainter(Brush.linearGradient(listOf(Palette.teal500, Palette.blue500, Palette.indigo500))) }
}

@Composable
fun AttachmentIconAction(label: String, icon: androidx.compose.ui.graphics.vector.ImageVector = PhosphorIcons.Regular.X) {
    AttachmentAction(onClick = {}, contentDescription = label) {
        Icon(icon, contentDescription = null)
    }
}

@Composable
fun AttachmentVertical() {
    val photo = attachmentPhoto()
    Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.Top) {
        Attachment(orientation = AttachmentOrientation.Vertical) {
            AttachmentMedia(photo, contentDescription = "Mountain lake")
            AttachmentActions { AttachmentIconAction("Remove") }
        }
        Attachment(orientation = AttachmentOrientation.Vertical) {
            AttachmentMedia { Icon(PhosphorIcons.Regular.FilePdf, contentDescription = null) }
            AttachmentContent {
                AttachmentTitle("Contract.pdf")
                AttachmentDescription("860 KB")
            }
        }
    }
}

Pressable

A card made pressable with AttachmentTrigger.

Web

Web preview

import { DownloadSimpleIcon } from "phosphor-react-native"
import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentLabel, AttachmentMedia, AttachmentTitle, AttachmentTrigger } from "@/components/ui/attachment"
import { Icon } from "@/components/ui/icon"

export function AttachmentPressableExample() {
  return (
    <Attachment orientation="horizontal" state="done">
      <AttachmentTrigger
        accessibilityLabel="Open Brand guidelines.pdf"
        onPress={() => undefined}
      />
      <AttachmentMedia variant="document">
        <AttachmentLabel className="bg-red-500">pdf</AttachmentLabel>
      </AttachmentMedia>
      <AttachmentContent>
        <AttachmentTitle>Brand guidelines.pdf</AttachmentTitle>
        <AttachmentDescription>5.8 MB</AttachmentDescription>
      </AttachmentContent>
      <AttachmentActions>
        <AttachmentAction accessibilityLabel="Download">
          <Icon as={DownloadSimpleIcon} />
        </AttachmentAction>
      </AttachmentActions>
    </Attachment>
  )
}
import EsuissUI
import PhosphorSwift
import SwiftUI

struct AttachmentPressableExample: View {
  var body: some View {
    Attachment {
      AttachmentTrigger("Open Brand guidelines.pdf") {}
      AttachmentMedia(.document) { AttachmentLabel("pdf", tint: EsuissColor.destructive) }
      AttachmentContent {
        AttachmentTitle("Brand guidelines.pdf")
        AttachmentDescription("5.8 MB")
      }
      AttachmentActions {
        AttachmentAction("Download", icon: Ph.downloadSimple.bold) {}
      }
    }
  }
}
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.contentDescription
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.DownloadSimple
import com.adamglin.phosphoricons.regular.X
import com.esuiss.ui.components.Attachment
import com.esuiss.ui.components.AttachmentAction
import com.esuiss.ui.components.AttachmentActions
import com.esuiss.ui.components.AttachmentContent
import com.esuiss.ui.components.AttachmentDescription
import com.esuiss.ui.components.AttachmentLabel
import com.esuiss.ui.components.AttachmentMedia
import com.esuiss.ui.components.AttachmentMediaVariant
import com.esuiss.ui.components.AttachmentTitle
import com.esuiss.ui.components.Icon
import com.esuiss.ui.theme.Palette

@Composable
fun AttachmentDocument(extension: String, tone: Color) {
    AttachmentMedia(variant = AttachmentMediaVariant.Document) {
        AttachmentLabel(extension, background = tone)
    }
}

@Composable
fun AttachmentIconAction(label: String, icon: androidx.compose.ui.graphics.vector.ImageVector = PhosphorIcons.Regular.X) {
    AttachmentAction(onClick = {}, contentDescription = label) {
        Icon(icon, contentDescription = null)
    }
}

@Composable
fun AttachmentPressable() {
    Row {
        Attachment(onClick = {}, onClickLabel = "Open Brand guidelines.pdf") {
            AttachmentDocument("pdf", Palette.red500)
            AttachmentContent {
                AttachmentTitle("Brand guidelines.pdf")
                AttachmentDescription("5.8 MB")
            }
            AttachmentActions { AttachmentIconAction("Download", PhosphorIcons.Regular.DownloadSimple) }
        }
    }
}

Group

Several attachments in an AttachmentGroup, one of them uploading.

Web

Web preview

import { CheckCircleIcon, XIcon } from "phosphor-react-native"
import { Image as NativeImage, View } from "react-native"
import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentGroup, AttachmentLabel, AttachmentMedia, AttachmentProgress, AttachmentTitle } from "@/components/ui/attachment"
import { Icon } from "@/components/ui/icon"
import { Spinner } from "@/components/ui/spinner"

const imageUrl =
"https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&q=80"

function Thumbnail() {
  return (
    <NativeImage
      source={{ uri: imageUrl }}
      accessibilityLabel="Mountain lake"
      className="size-full"
      resizeMode="cover"
    />
  )
}

export function AttachmentGroupExample() {
  return (
    <AttachmentGroup>
      <Attachment orientation="horizontal" state="uploading">
        <AttachmentMedia variant="document">
          <AttachmentLabel className="bg-red-500">pdf</AttachmentLabel>
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Invoice_1042.pdf</AttachmentTitle>
          <AttachmentProgress value={64} accessibilityLabel="Uploading" />
        </AttachmentContent>
        <AttachmentActions>
          <AttachmentAction accessibilityLabel="Cancel upload">
            <Icon as={XIcon} />
          </AttachmentAction>
        </AttachmentActions>
      </Attachment>
      <Attachment orientation="horizontal" state="processing">
        <AttachmentMedia variant="document">
          <AttachmentLabel className="bg-blue-500">docx</AttachmentLabel>
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Contract.docx</AttachmentTitle>
          <AttachmentDescription>Processing…</AttachmentDescription>
        </AttachmentContent>
        <AttachmentActions>
          <Spinner size={20} />
        </AttachmentActions>
      </Attachment>
      <Attachment orientation="horizontal" state="done">
        <AttachmentMedia variant="document">
          <AttachmentLabel className="bg-green-500">xlsx</AttachmentLabel>
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Menu_October.xlsx</AttachmentTitle>
          <AttachmentDescription>2.4 MB</AttachmentDescription>
        </AttachmentContent>
        <AttachmentActions>
          <View accessible accessibilityLabel="Uploaded">
            <Icon
              as={CheckCircleIcon}
              weight="fill"
              className="size-6 text-green-600 dark:text-green-400"
            />
          </View>
        </AttachmentActions>
      </Attachment>
      <Attachment orientation="horizontal" state="done">
        <AttachmentMedia variant="image">
          <Thumbnail />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Table_12.jpg</AttachmentTitle>
          <AttachmentDescription>1.8 MB</AttachmentDescription>
        </AttachmentContent>
        <AttachmentActions>
          <AttachmentAction accessibilityLabel="Remove">
            <Icon as={XIcon} />
          </AttachmentAction>
        </AttachmentActions>
      </Attachment>
      <Attachment orientation="horizontal" state="error">
        <AttachmentMedia variant="document">
          <AttachmentLabel className="bg-red-500">pdf</AttachmentLabel>
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Z-Report_30-09.pdf</AttachmentTitle>
          <AttachmentDescription>Upload failed</AttachmentDescription>
        </AttachmentContent>
        <AttachmentActions>
          <AttachmentAction variant="link">Try Again</AttachmentAction>
        </AttachmentActions>
      </Attachment>
    </AttachmentGroup>
  )
}
import EsuissUI
import PhosphorSwift
import SwiftUI

struct AttachmentGroupExample: View {
  var body: some View {
    AttachmentGroup {
      Attachment(state: .uploading) {
        AttachmentMedia(.document) { AttachmentLabel("pdf", tint: EsuissColor.destructive) }
        AttachmentContent {
          AttachmentTitle("Invoice_1042.pdf")
          AttachmentProgress(value: 64, label: "Uploading")
        }
        AttachmentActions {
          AttachmentAction("Cancel upload", icon: Ph.x.bold) {}
        }
      }
      Attachment(state: .processing) {
        AttachmentMedia(.document) { AttachmentLabel("docx", tint: EsuissColor.primary) }
        AttachmentContent {
          AttachmentTitle("Contract.docx")
          AttachmentDescription("Processing…")
        }
        AttachmentActions {
          Spinner(dimension: 20)
        }
      }
      Attachment {
        AttachmentMedia(.document) { AttachmentLabel("xlsx", tint: Palette.green500) }
        AttachmentContent {
          AttachmentTitle("Menu_October.xlsx")
          AttachmentDescription("2.4 MB")
        }
        AttachmentActions {
          Ph.checkCircle.fill
            .frame(width: 24, height: 24)
            .foregroundStyle(Color.adaptive(light: Palette.green600, dark: Palette.green400))
            .accessibilityElement()
            .accessibilityLabel(Text("Uploaded"))
        }
      }
      Attachment {
        AttachmentThumbnail()
        AttachmentContent {
          AttachmentTitle("Table_12.jpg")
          AttachmentDescription("1.8 MB")
        }
        AttachmentActions {
          AttachmentAction("Remove", icon: Ph.x.bold) {}
        }
      }
      Attachment(state: .error) {
        AttachmentMedia(.document) { AttachmentLabel("pdf", tint: EsuissColor.destructive) }
        AttachmentContent {
          AttachmentTitle("Z-Report_30-09.pdf")
          AttachmentDescription("Upload failed")
        }
        AttachmentActions {
          AttachmentAction("Try Again") {}
        }
      }
    }
  }
}

struct AttachmentThumbnail: View {
  var body: some View {
    AttachmentMedia(url: URL(string: "https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&q=80"))
      .accessibilityLabel(Text("Mountain lake"))
  }
}
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.BrushPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Fill
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.fill.CheckCircle
import com.adamglin.phosphoricons.regular.X
import com.esuiss.ui.components.Attachment
import com.esuiss.ui.components.AttachmentAction
import com.esuiss.ui.components.AttachmentActions
import com.esuiss.ui.components.AttachmentContent
import com.esuiss.ui.components.AttachmentDescription
import com.esuiss.ui.components.AttachmentGroup
import com.esuiss.ui.components.AttachmentLabel
import com.esuiss.ui.components.AttachmentMedia
import com.esuiss.ui.components.AttachmentMediaVariant
import com.esuiss.ui.components.AttachmentProgress
import com.esuiss.ui.components.AttachmentState
import com.esuiss.ui.components.AttachmentTitle
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Spinner
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Palette

@Composable
fun attachmentPhoto(): Painter {
    val colors = EsuissTheme.colors
    return remember(colors) { BrushPainter(Brush.linearGradient(listOf(Palette.teal500, Palette.blue500, Palette.indigo500))) }
}

@Composable
fun AttachmentDocument(extension: String, tone: Color) {
    AttachmentMedia(variant = AttachmentMediaVariant.Document) {
        AttachmentLabel(extension, background = tone)
    }
}

@Composable
fun AttachmentIconAction(label: String, icon: androidx.compose.ui.graphics.vector.ImageVector = PhosphorIcons.Regular.X) {
    AttachmentAction(onClick = {}, contentDescription = label) {
        Icon(icon, contentDescription = null)
    }
}

@Composable
fun AttachmentGroupStory() {
    val colors = EsuissTheme.colors
    val photo = attachmentPhoto()
    AttachmentGroup {
        Attachment(state = AttachmentState.Uploading) {
            AttachmentDocument("pdf", Palette.red500)
            AttachmentContent {
                AttachmentTitle("Invoice_1042.pdf")
                AttachmentProgress(64f, contentDescription = "Uploading")
            }
            AttachmentActions { AttachmentIconAction("Cancel upload") }
        }
        Attachment(state = AttachmentState.Processing) {
            AttachmentDocument("docx", Palette.blue500)
            AttachmentContent {
                AttachmentTitle("Contract.docx")
                AttachmentDescription("Processing…")
            }
            AttachmentActions { Spinner(size = 20.dp) }
        }
        Attachment {
            AttachmentDocument("xlsx", Palette.green500)
            AttachmentContent {
                AttachmentTitle("Menu_October.xlsx")
                AttachmentDescription("2.4 MB")
            }
            AttachmentActions {
                Box(Modifier.clearAndSetSemantics { contentDescription = "Uploaded" }) {
                    Icon(PhosphorIcons.Fill.CheckCircle, contentDescription = null, size = 24.dp, tint = if (colors.isDark) Palette.green400 else Palette.green600)
                }
            }
        }
        Attachment {
            AttachmentMedia(photo, contentDescription = "Mountain lake")
            AttachmentContent {
                AttachmentTitle("Table_12.jpg")
                AttachmentDescription("1.8 MB")
            }
            AttachmentActions { AttachmentIconAction("Remove") }
        }
        Attachment(state = AttachmentState.Error) {
            AttachmentDocument("pdf", Palette.red500)
            AttachmentContent {
                AttachmentTitle("Z-Report_30-09.pdf")
                AttachmentDescription("Upload failed")
            }
            AttachmentActions { AttachmentAction("Try Again", onClick = {}) }
        }
    }
}

File types

Document media with colored extension labels.

Web

Web preview

import { View } from "react-native"
import { Attachment, AttachmentContent, AttachmentDescription, AttachmentLabel, AttachmentMedia, AttachmentTitle } from "@/components/ui/attachment"

export function AttachmentFileTypesExample() {
  return (
    <View className="items-start gap-3">
      {[
        {
          name: "Invoice.pdf",
          meta: "128 KB",
          extension: "pdf",
          tone: "bg-red-500",
        },
        {
          name: "Menu.xlsx",
          meta: "2.4 MB",
          extension: "xlsx",
          tone: "bg-green-500",
        },
        {
          name: "Contract.docx",
          meta: "84 KB",
          extension: "docx",
          tone: "bg-blue-500",
        },
        {
          name: "Pitch.key",
          meta: "12 MB",
          extension: "key",
          tone: "bg-orange-500",
        },
        { name: "Archive.zip", meta: "48 MB", extension: "zip", tone: "" },
      ].map((file) => (
        <Attachment key={file.name} orientation="horizontal" state="done">
          <AttachmentMedia variant="document">
            <AttachmentLabel className={file.tone}>
              {file.extension}
            </AttachmentLabel>
          </AttachmentMedia>
          <AttachmentContent>
            <AttachmentTitle>{file.name}</AttachmentTitle>
            <AttachmentDescription>{file.meta}</AttachmentDescription>
          </AttachmentContent>
        </Attachment>
      ))}
    </View>
  )
}
import EsuissUI
import SwiftUI

struct AttachmentFileTypesExample: View {
  var body: some View {
    VStack(alignment: .leading, spacing: 12) {
      ForEach(AttachmentFile.all, id: \.name) { file in
        Attachment {
          AttachmentMedia(.document) { AttachmentLabel(verbatim: file.ext, tint: file.tint) }
          AttachmentContent {
            AttachmentTitle(verbatim: file.name)
            AttachmentDescription(verbatim: file.meta)
          }
        }
      }
    }
  }
}

struct AttachmentFile {
  let name: String
  let meta: String
  let ext: String
  let tint: Color

  static let all: [AttachmentFile] = [
    AttachmentFile(name: "Invoice.pdf", meta: "128 KB", ext: "pdf", tint: EsuissColor.destructive),
    AttachmentFile(name: "Menu.xlsx", meta: "2.4 MB", ext: "xlsx", tint: Palette.green500),
    AttachmentFile(name: "Contract.docx", meta: "84 KB", ext: "docx", tint: EsuissColor.primary),
    AttachmentFile(name: "Pitch.key", meta: "12 MB", ext: "key", tint: Palette.amber500),
    AttachmentFile(name: "Archive.zip", meta: "48 MB", ext: "zip", tint: EsuissColor.primary),
  ]
}

let all: [AttachmentFile] = [
  AttachmentFile(name: "Invoice.pdf", meta: "128 KB", ext: "pdf", tint: EsuissColor.destructive),
  AttachmentFile(name: "Menu.xlsx", meta: "2.4 MB", ext: "xlsx", tint: Palette.green500),
  AttachmentFile(name: "Contract.docx", meta: "84 KB", ext: "docx", tint: EsuissColor.primary),
  AttachmentFile(name: "Pitch.key", meta: "12 MB", ext: "key", tint: Palette.amber500),
  AttachmentFile(name: "Archive.zip", meta: "48 MB", ext: "zip", tint: EsuissColor.primary),
]
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Attachment
import com.esuiss.ui.components.AttachmentContent
import com.esuiss.ui.components.AttachmentDescription
import com.esuiss.ui.components.AttachmentLabel
import com.esuiss.ui.components.AttachmentMedia
import com.esuiss.ui.components.AttachmentMediaVariant
import com.esuiss.ui.components.AttachmentTitle
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Palette

@Composable
fun AttachmentDocument(extension: String, tone: Color) {
    AttachmentMedia(variant = AttachmentMediaVariant.Document) {
        AttachmentLabel(extension, background = tone)
    }
}

class AttachmentFile(val name: String, val meta: String, val extension: String, val tone: @Composable () -> Color)

val attachmentFiles = listOf(
    AttachmentFile("Invoice.pdf", "128 KB", "pdf") { Palette.red500 },
    AttachmentFile("Menu.xlsx", "2.4 MB", "xlsx") { Palette.green500 },
    AttachmentFile("Contract.docx", "84 KB", "docx") { Palette.blue500 },
    AttachmentFile("Pitch.key", "12 MB", "key") { Palette.orange500 },
    AttachmentFile("Archive.zip", "48 MB", "zip") { EsuissTheme.colors.primary },
)

@Composable
fun AttachmentFileTypes() {
    Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
        attachmentFiles.forEach { file ->
            Attachment {
                AttachmentDocument(file.extension, file.tone())
                AttachmentContent {
                    AttachmentTitle(file.name)
                    AttachmentDescription(file.meta)
                }
            }
        }
    }
}

API Reference

01

Attachment

Card that provides state, size and orientation to its parts.

PropTypeDefaultDescription
stateAttachmentState"done"idle, uploading, processing, error or done. Defaults to done.
sizeAttachmentSize"default"default, sm or xs.
orientationAttachmentOrientation"horizontal"horizontal or vertical.

Also accepts the props of ViewProps.

02

AttachmentMedia

Thumbnail area for an icon, document shape or image.

PropTypeDefaultDescription
variant"icon" | "document" | "image""icon"icon, document or image.

Also accepts the props of ViewProps.

03

AttachmentLabel

Small uppercase tag, such as a file extension.

Also accepts the props of ViewProps.

04

AttachmentContent

Column for the title, description and progress.

Also accepts the props of ViewProps.

05

AttachmentTitle

File name, one line by default.

Also accepts the props of React.ComponentProps<typeof Text>.

06

AttachmentDescription

Secondary details, red in the error state.

Also accepts the props of React.ComponentProps<typeof Text>.

07

AttachmentProgress

Progress bar with a percentage value.

Also accepts the props of React.ComponentProps<typeof ProgressPrimitive.Root>.

08

AttachmentActions

Row of actions; floats over the media in vertical layout.

Also accepts the props of ViewProps.

09

AttachmentAction

Small button sized to the attachment.

Also accepts the props of React.ComponentProps<typeof Button>.

10

AttachmentTrigger

Pressable overlay that makes the whole card pressable.

PropTypeDefaultDescription
renderRenderProp<Record<string, unknown>, Record<string, never>>-

Also accepts the props of React.ComponentProps<typeof Pressable>, React.RefAttributes<View>.

11

AttachmentGroup

List container that joins attachments with separators.

Also accepts the props of ViewProps.

Accessibility

  • AttachmentProgress has the progressbar role with its value exposed.
  • AttachmentTrigger has the button role; give it an accessibilityLabel such as "Open Invoice.pdf".
  • Icon-only actions need an accessibilityLabel.

Notes

  • Uses bg-muted, border-border, bg-primary and text-destructive tokens; light and dark follow the system appearance.
  • Separators and action positions use start and end offsets, so they flip in right-to-left layouts.
Web

Web preview

import { XIcon } from "phosphor-react-native"
import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentLabel, AttachmentMedia, AttachmentTitle } from "@/components/ui/attachment"
import { Icon } from "@/components/ui/icon"

export function AttachmentExample() {
  return (
    <Attachment orientation="horizontal" state="done">
      <AttachmentMedia variant="document">
        <AttachmentLabel className="bg-red-500">pdf</AttachmentLabel>
      </AttachmentMedia>
      <AttachmentContent>
        <AttachmentTitle>Quarterly report.pdf</AttachmentTitle>
        <AttachmentDescription>2.4 MB</AttachmentDescription>
      </AttachmentContent>
      <AttachmentActions>
        <AttachmentAction accessibilityLabel="Remove">
          <Icon as={XIcon} />
        </AttachmentAction>
      </AttachmentActions>
    </Attachment>
  )
}
import EsuissUI
import PhosphorSwift
import SwiftUI

struct AttachmentExample: View {
  var body: some View {
    Attachment {
      AttachmentMedia(.document) { AttachmentLabel("pdf", tint: EsuissColor.destructive) }
      AttachmentContent {
        AttachmentTitle("Quarterly report.pdf")
        AttachmentDescription("2.4 MB")
      }
      AttachmentActions {
        AttachmentAction("Remove", icon: Ph.x.bold) {}
      }
    }
  }
}
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.contentDescription
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.X
import com.esuiss.ui.components.Attachment
import com.esuiss.ui.components.AttachmentAction
import com.esuiss.ui.components.AttachmentActions
import com.esuiss.ui.components.AttachmentContent
import com.esuiss.ui.components.AttachmentDescription
import com.esuiss.ui.components.AttachmentLabel
import com.esuiss.ui.components.AttachmentMedia
import com.esuiss.ui.components.AttachmentMediaVariant
import com.esuiss.ui.components.AttachmentTitle
import com.esuiss.ui.components.Icon
import com.esuiss.ui.theme.Palette

@Composable
fun AttachmentDocument(extension: String, tone: Color) {
    AttachmentMedia(variant = AttachmentMediaVariant.Document) {
        AttachmentLabel(extension, background = tone)
    }
}

@Composable
fun AttachmentIconAction(label: String, icon: androidx.compose.ui.graphics.vector.ImageVector = PhosphorIcons.Regular.X) {
    AttachmentAction(onClick = {}, contentDescription = label) {
        Icon(icon, contentDescription = null)
    }
}

@Composable
fun AttachmentDefault() {
    Row {
        Attachment {
            AttachmentDocument("pdf", Palette.red500)
            AttachmentContent {
                AttachmentTitle("Quarterly report.pdf")
                AttachmentDescription("2.4 MB")
            }
            AttachmentActions { AttachmentIconAction("Remove") }
        }
    }
}
import { XIcon } from "@phosphor-icons/react"
import {
  Attachment,
  AttachmentAction,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentLabel,
  AttachmentMedia,
  AttachmentTitle,
} from "@/components/ui/attachment"

export default function AttachmentDemo() {
  return (
    <Attachment orientation="horizontal" state="done">
      <AttachmentMedia variant="document">
        <AttachmentLabel className="bg-red-600">pdf</AttachmentLabel>
      </AttachmentMedia>
      <AttachmentContent>
        <AttachmentTitle>Quarterly report.pdf</AttachmentTitle>
        <AttachmentDescription>2.4 MB</AttachmentDescription>
      </AttachmentContent>
      <AttachmentActions>
        <AttachmentAction aria-label="Remove">
          <XIcon />
        </AttachmentAction>
      </AttachmentActions>
    </Attachment>
  )
}
components/ui/attachment.tsx
import { mergeProps } from "@base-ui/react/merge-props"
import { Progress as ProgressPrimitive } from "@base-ui/react/progress"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import type * as React from "react"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"

const attachmentVariants = cva(
  "group/attachment relative flex w-fit max-w-full min-w-0 shrink-0 flex-wrap rounded-2xl bg-muted text-foreground transition-colors has-[[data-slot=attachment-trigger]:focus-visible]:ring-3 has-[[data-slot=attachment-trigger]:focus-visible]:ring-ring/50 has-[>a,>button]:hover:bg-[color-mix(in_oklab,var(--muted),var(--foreground)_5%)] not-in-data-[slot=attachment-group]:data-[state=idle]:border not-in-data-[slot=attachment-group]:data-[state=idle]:border-dashed not-in-data-[slot=attachment-group]:data-[state=idle]:border-input not-in-data-[slot=attachment-group]:data-[state=idle]:bg-transparent not-in-data-[slot=attachment-group]:data-[state=idle]:has-[>a,>button]:hover:bg-accent in-data-[slot=attachment-group]:w-full in-data-[slot=attachment-group]:rounded-none in-data-[slot=attachment-group]:not-first:before:absolute in-data-[slot=attachment-group]:not-first:before:inset-e-0 in-data-[slot=attachment-group]:not-first:before:top-0 in-data-[slot=attachment-group]:not-first:before:start-4 in-data-[slot=attachment-group]:not-first:before:border-t in-data-[slot=attachment-group]:not-first:before:border-border in-data-[slot=attachment-group]:not-first:has-data-[slot=attachment-media]:before:start-16 not-in-data-[slot=attachment-group]:data-[state=idle]:has-[>a,>button]:hover:text-accent-foreground",
  {
    variants: {
      size: {
        default:
          "gap-3 px-4 py-3 text-sm data-[orientation=vertical]:gap-2 data-[orientation=vertical]:has-data-[slot=attachment-content]:px-2.5 data-[orientation=vertical]:has-data-[slot=attachment-content]:py-2 data-[orientation=vertical]:has-data-[slot=attachment-media]:p-2",
        sm: "gap-2.5 text-xs has-data-[slot=attachment-content]:px-2 has-data-[slot=attachment-content]:py-1.5 has-data-[slot=attachment-media]:p-1.5 in-data-[slot=attachment-group]:not-first:before:start-2 in-data-[slot=attachment-group]:not-first:has-data-[slot=attachment-media]:before:start-12",
import { FileTextIcon, XIcon } from "@phosphor-icons/react"
import {
  Attachment,
  AttachmentAction,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentMedia,
  AttachmentTitle,
} from "@/components/ui/attachment"

export default function AttachmentSizes() {
  return (
    <div className="flex flex-col items-start gap-3">
      {(["default", "sm", "xs"] as const).map((size) => (
        <Attachment
          key={size}
          orientation="horizontal"
          state="done"
          size={size}
        >
          <AttachmentMedia>
            <FileTextIcon />
          </AttachmentMedia>
          <AttachmentContent>
            <AttachmentTitle>Meeting notes.docx</AttachmentTitle>
            <AttachmentDescription>184 KB</AttachmentDescription>
          </AttachmentContent>
          <AttachmentActions>
            <AttachmentAction aria-label="Remove">
              <XIcon />
            </AttachmentAction>
          </AttachmentActions>
        </Attachment>
      ))}
    </div>
  )
}
import { UploadSimpleIcon, XIcon } from "@phosphor-icons/react"
import {
  Attachment,
  AttachmentAction,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentLabel,
  AttachmentMedia,
  AttachmentProgress,
  AttachmentTitle,
} from "@/components/ui/attachment"
import { Spinner } from "@/components/ui/spinner"

export default function AttachmentStates() {
  return (
    <div className="flex w-96 flex-col gap-3">
      <Attachment orientation="horizontal" state="idle" className="w-full">
        <AttachmentMedia>
          <UploadSimpleIcon />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Drop a file here</AttachmentTitle>
          <AttachmentDescription>
            PDF, PNG or JPG up to 10 MB
          </AttachmentDescription>
        </AttachmentContent>
        <AttachmentActions>
          <AttachmentAction variant="link">Choose File</AttachmentAction>
        </AttachmentActions>
      </Attachment>
      <Attachment orientation="horizontal" state="uploading" className="w-full">
        <AttachmentMedia variant="document">
          <AttachmentLabel className="bg-red-600">pdf</AttachmentLabel>
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Invoice March.pdf</AttachmentTitle>
          <AttachmentProgress value={64} aria-label="Uploading" />
        </AttachmentContent>
        <AttachmentActions>
          <AttachmentAction aria-label="Cancel upload">
            <XIcon />
          </AttachmentAction>
        </AttachmentActions>
      </Attachment>
      <Attachment
        orientation="horizontal"
        state="processing"
        className="w-full"
      >
        <AttachmentMedia variant="document">
          <AttachmentLabel className="bg-red-600">pdf</AttachmentLabel>
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Invoice March.pdf</AttachmentTitle>
          <AttachmentDescription>Processing…</AttachmentDescription>
        </AttachmentContent>
        <AttachmentActions>
          <Spinner className="size-5 text-muted-foreground" />
        </AttachmentActions>
      </Attachment>
      <Attachment orientation="horizontal" state="error" className="w-full">
        <AttachmentMedia variant="document">
          <AttachmentLabel className="bg-orange-700">key</AttachmentLabel>
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Presentation.key</AttachmentTitle>
          <AttachmentDescription>File type not supported</AttachmentDescription>
        </AttachmentContent>
        <AttachmentActions>
          <AttachmentAction aria-label="Remove">
            <XIcon />
          </AttachmentAction>
        </AttachmentActions>
      </Attachment>
      <Attachment orientation="horizontal" state="done" className="w-full">
        <AttachmentMedia variant="document">
          <AttachmentLabel className="bg-red-600">pdf</AttachmentLabel>
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Invoice March.pdf</AttachmentTitle>
          <AttachmentDescription>1.1 MB</AttachmentDescription>
        </AttachmentContent>
      </Attachment>
    </div>
  )
}
import { UploadSimpleIcon } from "@phosphor-icons/react"
import {
  Attachment,
  AttachmentContent,
  AttachmentDescription,
  AttachmentMedia,
  AttachmentTitle,
  AttachmentTrigger,
} from "@/components/ui/attachment"

export default function AttachmentDropZone() {
  return (
    <Attachment orientation="horizontal" state="idle" className="w-96">
      <AttachmentTrigger aria-label="Choose a file to upload" />
      <AttachmentMedia>
        <UploadSimpleIcon />
      </AttachmentMedia>
      <AttachmentContent>
        <AttachmentTitle>Drop a file or click to choose</AttachmentTitle>
        <AttachmentDescription>
          PDF, PNG or JPG up to 10 MB
        </AttachmentDescription>
      </AttachmentContent>
    </Attachment>
  )
}
import { XIcon } from "@phosphor-icons/react"
import {
  Attachment,
  AttachmentAction,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentMedia,
  AttachmentTitle,
} from "@/components/ui/attachment"

const imageUrl =
  "https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&q=80"

export default function AttachmentImage() {
  return (
    <Attachment orientation="horizontal" state="done">
      <AttachmentMedia variant="image">
        <img src={imageUrl} alt="Mountain lake" />
      </AttachmentMedia>
      <AttachmentContent>
        <AttachmentTitle>Lake.jpg</AttachmentTitle>
        <AttachmentDescription>3.2 MB</AttachmentDescription>
      </AttachmentContent>
      <AttachmentActions>
        <AttachmentAction aria-label="Remove">
          <XIcon />
        </AttachmentAction>
      </AttachmentActions>
    </Attachment>
  )
}
import { FilePdfIcon, XIcon } from "@phosphor-icons/react"
import {
  Attachment,
  AttachmentAction,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentMedia,
  AttachmentTitle,
} from "@/components/ui/attachment"

const imageUrl =
  "https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&q=80"

export default function AttachmentVertical() {
  return (
    <div className="flex items-start gap-3">
      <Attachment orientation="vertical" state="done">
        <AttachmentMedia variant="image">
          <img src={imageUrl} alt="Mountain lake" />
        </AttachmentMedia>
        <AttachmentActions>
          <AttachmentAction variant="secondary" aria-label="Remove">
            <XIcon />
          </AttachmentAction>
        </AttachmentActions>
      </Attachment>
      <Attachment orientation="vertical" state="done">
        <AttachmentMedia>
          <FilePdfIcon />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Contract.pdf</AttachmentTitle>
          <AttachmentDescription>860 KB</AttachmentDescription>
        </AttachmentContent>
      </Attachment>
    </div>
  )
}
import { CheckCircleIcon, XIcon } from "@phosphor-icons/react"
import {
  Attachment,
  AttachmentAction,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentGroup,
  AttachmentLabel,
  AttachmentMedia,
  AttachmentProgress,
  AttachmentTitle,
} from "@/components/ui/attachment"
import { Spinner } from "@/components/ui/spinner"

const imageUrl =
  "https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&q=80"

export default function AttachmentGroupExample() {
  return (
    <div className="w-96">
      <AttachmentGroup>
        <Attachment orientation="horizontal" state="uploading">
          <AttachmentMedia variant="document">
            <AttachmentLabel className="bg-red-600">pdf</AttachmentLabel>
          </AttachmentMedia>
          <AttachmentContent>
            <AttachmentTitle>Invoice_1042.pdf</AttachmentTitle>
            <AttachmentProgress value={64} aria-label="Uploading" />
          </AttachmentContent>
          <AttachmentActions>
            <AttachmentAction aria-label="Cancel upload">
              <XIcon />
            </AttachmentAction>
          </AttachmentActions>
        </Attachment>
        <Attachment orientation="horizontal" state="processing">
          <AttachmentMedia variant="document">
            <AttachmentLabel className="bg-blue-600">docx</AttachmentLabel>
          </AttachmentMedia>
          <AttachmentContent>
            <AttachmentTitle>Contract.docx</AttachmentTitle>
            <AttachmentDescription>Processing…</AttachmentDescription>
          </AttachmentContent>
          <AttachmentActions>
            <Spinner className="size-5 text-muted-foreground" />
          </AttachmentActions>
        </Attachment>
        <Attachment orientation="horizontal" state="done">
          <AttachmentMedia variant="document">
            <AttachmentLabel className="bg-green-700">xlsx</AttachmentLabel>
          </AttachmentMedia>
          <AttachmentContent>
            <AttachmentTitle>Menu_October.xlsx</AttachmentTitle>
            <AttachmentDescription>2.4 MB</AttachmentDescription>
          </AttachmentContent>
          <AttachmentActions>
            <CheckCircleIcon
              aria-label="Uploaded"
              className="size-6 fill-green-600 text-white dark:fill-green-400"
            />
          </AttachmentActions>
        </Attachment>
        <Attachment orientation="horizontal" state="done">
          <AttachmentMedia variant="image">
            <img src={imageUrl} alt="Mountain lake" />
          </AttachmentMedia>
          <AttachmentContent>
            <AttachmentTitle>Table_12.jpg</AttachmentTitle>
            <AttachmentDescription>1.8 MB</AttachmentDescription>
          </AttachmentContent>
          <AttachmentActions>
            <AttachmentAction aria-label="Remove">
              <XIcon />
            </AttachmentAction>
          </AttachmentActions>
        </Attachment>
        <Attachment orientation="horizontal" state="error">
          <AttachmentMedia variant="document">
            <AttachmentLabel className="bg-red-600">pdf</AttachmentLabel>
          </AttachmentMedia>
          <AttachmentContent>
            <AttachmentTitle>Z-Report_30-09.pdf</AttachmentTitle>
            <AttachmentDescription>Upload failed</AttachmentDescription>
          </AttachmentContent>
          <AttachmentActions>
            <AttachmentAction variant="link">Try Again</AttachmentAction>
          </AttachmentActions>
        </Attachment>
      </AttachmentGroup>
    </div>
  )
}
import { DownloadSimpleIcon } from "@phosphor-icons/react"
import {
  Attachment,
  AttachmentAction,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentLabel,
  AttachmentMedia,
  AttachmentTitle,
  AttachmentTrigger,
} from "@/components/ui/attachment"

export default function AttachmentClickable() {
  return (
    <Attachment orientation="horizontal" state="done">
      <AttachmentTrigger aria-label="Open Brand guidelines.pdf" />
      <AttachmentMedia variant="document">
        <AttachmentLabel className="bg-red-600">pdf</AttachmentLabel>
      </AttachmentMedia>
      <AttachmentContent>
        <AttachmentTitle>Brand guidelines.pdf</AttachmentTitle>
        <AttachmentDescription>5.8 MB</AttachmentDescription>
      </AttachmentContent>
      <AttachmentActions>
        <AttachmentAction aria-label="Download">
          <DownloadSimpleIcon />
        </AttachmentAction>
      </AttachmentActions>
    </Attachment>
  )
}
import {
  Attachment,
  AttachmentContent,
  AttachmentDescription,
  AttachmentLabel,
  AttachmentMedia,
  AttachmentTitle,
} from "@/components/ui/attachment"

export default function AttachmentFileTypes() {
  return (
    <div className="flex flex-col gap-3">
      {[
        {
          name: "Invoice.pdf",
          meta: "128 KB",
          extension: "pdf",
          tone: "bg-red-600",
        },
        {
          name: "Menu.xlsx",
          meta: "2.4 MB",
          extension: "xlsx",
          tone: "bg-green-700",
        },
        {
          name: "Contract.docx",
          meta: "84 KB",
          extension: "docx",
          tone: "bg-blue-600",
        },
        {
          name: "Pitch.key",
          meta: "12 MB",
          extension: "key",
          tone: "bg-orange-700",
        },
        { name: "Archive.zip", meta: "48 MB", extension: "zip", tone: "" },
      ].map((file) => (
        <Attachment key={file.name} orientation="horizontal" state="done">
          <AttachmentMedia variant="document">
            <AttachmentLabel className={file.tone}>
              {file.extension}
            </AttachmentLabel>
          </AttachmentMedia>
          <AttachmentContent>
            <AttachmentTitle>{file.name}</AttachmentTitle>
            <AttachmentDescription>{file.meta}</AttachmentDescription>
          </AttachmentContent>
        </Attachment>
      ))}
      <Attachment orientation="horizontal" state="done">
        <AttachmentMedia variant="document">
          <AttachmentLabel className="bg-blue-600">MD</AttachmentLabel>
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>README.md</AttachmentTitle>
          <AttachmentDescription>4 KB</AttachmentDescription>
        </AttachmentContent>
      </Attachment>
    </div>
  )
}
import { XIcon } from "@phosphor-icons/react"
import {
  Attachment,
  AttachmentAction,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentLabel,
  AttachmentMedia,
  AttachmentTitle,
} from "@/components/ui/attachment"

const imageUrl =
  "https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&q=80"

export default function AttachmentFileList() {
  return (
    <div className="flex w-96 flex-col gap-3">
      <Attachment orientation="horizontal" state="done" className="w-full">
        <AttachmentMedia variant="document">
          <AttachmentLabel className="bg-red-600">pdf</AttachmentLabel>
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Invoice_1042.pdf</AttachmentTitle>
          <AttachmentDescription>128 KB</AttachmentDescription>
        </AttachmentContent>
        <AttachmentActions>
          <AttachmentAction aria-label="Remove">
            <XIcon />
          </AttachmentAction>
        </AttachmentActions>
      </Attachment>
      <Attachment orientation="horizontal" state="done" className="w-full">
        <AttachmentMedia variant="document">
          <AttachmentLabel className="bg-green-700">xlsx</AttachmentLabel>
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Menu_October.xlsx</AttachmentTitle>
          <AttachmentDescription>2.4 MB</AttachmentDescription>
        </AttachmentContent>
        <AttachmentActions>
          <AttachmentAction aria-label="Remove">
            <XIcon />
          </AttachmentAction>
        </AttachmentActions>
      </Attachment>
      <Attachment orientation="horizontal" state="done" className="w-full">
        <AttachmentMedia variant="image">
          <img src={imageUrl} alt="Mountain lake" />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Table_12.jpg</AttachmentTitle>
          <AttachmentDescription>1.8 MB</AttachmentDescription>
        </AttachmentContent>
        <AttachmentActions>
          <AttachmentAction aria-label="Remove">
            <XIcon />
          </AttachmentAction>
        </AttachmentActions>
      </Attachment>
      <Attachment orientation="horizontal" state="error" className="w-full">
        <AttachmentMedia variant="document">
          <AttachmentLabel className="bg-red-600">pdf</AttachmentLabel>
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Z-Report_30-09.pdf</AttachmentTitle>
          <AttachmentDescription>Upload failed</AttachmentDescription>
        </AttachmentContent>
        <AttachmentActions>
          <AttachmentAction variant="link">Try Again</AttachmentAction>
        </AttachmentActions>
      </Attachment>
    </div>
  )
}
import { CheckCircleIcon } from "@phosphor-icons/react"
import {
  Attachment,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentLabel,
  AttachmentMedia,
  AttachmentTitle,
} from "@/components/ui/attachment"

const imageUrl =
  "https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&q=80"

export default function AttachmentUploaded() {
  return (
    <div className="flex w-96 flex-col gap-3">
      <Attachment orientation="horizontal" state="done" className="w-full">
        <AttachmentMedia variant="document">
          <AttachmentLabel className="bg-green-700">xlsx</AttachmentLabel>
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Menu_October.xlsx</AttachmentTitle>
          <AttachmentDescription>2.4 MB</AttachmentDescription>
        </AttachmentContent>
        <AttachmentActions>
          <CheckCircleIcon
            aria-label="Uploaded"
            className="size-6 fill-green-600 text-white dark:fill-green-400"
          />
        </AttachmentActions>
      </Attachment>
      <Attachment orientation="horizontal" state="done" className="w-full">
        <AttachmentMedia variant="image">
          <img src={imageUrl} alt="Mountain lake" />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>Table_12.jpg</AttachmentTitle>
          <AttachmentDescription>1.8 MB</AttachmentDescription>
        </AttachmentContent>
        <AttachmentActions>
          <CheckCircleIcon
            aria-label="Uploaded"
            className="size-6 fill-green-600 text-white dark:fill-green-400"
          />
        </AttachmentActions>
      </Attachment>
    </div>
  )
}
uniwind/components/ui/attachment.tsx
import { cn } from "cn"
import * as React from "react"
import { Pressable, View, type ViewProps } from "react-native"
import * as ProgressPrimitive from "@/components/primitives/progress"
import type { RenderProp } from "@/components/primitives/types"
import { resolveClassName, useRender } from "@/components/primitives/use-render"
import { Button, buttonTextVariants } from "@/components/ui/button"
import { IconClassContext } from "@/components/ui/icon"
import {
  ProgressIndicator,
  ProgressTrack,
  ProgressValue,
} from "@/components/ui/progress"
import { Text, TextClassContext } from "@/components/ui/text"

type AttachmentState = "idle" | "uploading" | "processing" | "error" | "done"
swift/Components/Attachment.swift
import SwiftUI

public enum AttachmentState: CaseIterable, Sendable {
  case idle, uploading, processing, error, done
}

public enum AttachmentSize: CaseIterable, Sendable {
  case `default`, sm, xs
}

public enum AttachmentOrientation: CaseIterable, Sendable {
  case horizontal, vertical
}

public enum AttachmentMediaVariant: CaseIterable, Sendable {
  case icon, document, image
swift/Components/Avatar.swift
import SwiftUI

public enum AvatarSize: CaseIterable, Sendable {
  case sm, `default`, lg, xl

  var dimension: CGFloat {
    switch self {
    case .sm: 24
    case .default: 32
    case .lg: 40
    case .xl: 64
    }
  }

  var overlap: CGFloat {
    self == .xl ? 16 : 8
kotlin/components/Attachment.kt
package com.esuiss.ui.components

import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
stylesheet/components/ui/attachment.tsx
import * as React from "react"
import {
  Pressable,
  type StyleProp,
  StyleSheet,
  View,
  type ViewProps,
  type ViewStyle,
} from "react-native"
import * as ProgressPrimitive from "@/components/primitives/progress"
import type { RenderProp } from "@/components/primitives/types"
import { useRender } from "@/components/primitives/use-render"
import { Button, buttonTextStyle } from "@/components/ui/button"
import { IconSizeContext } from "@/components/ui/icon"
import {
  ProgressIndicator,
nativewind/components/ui/attachment.tsx
import { cn } from "cn"
import * as React from "react"
import { Pressable, View, type ViewProps } from "react-native"
import * as ProgressPrimitive from "@/components/primitives/progress"
import type { RenderProp } from "@/components/primitives/types"
import { resolveClassName, useRender } from "@/components/primitives/use-render"
import { Button, buttonTextVariants } from "@/components/ui/button"
import { IconClassContext } from "@/components/ui/icon"
import {
  ProgressIndicator,
  ProgressTrack,
  ProgressValue,
} from "@/components/ui/progress"
import { Text, TextClassContext } from "@/components/ui/text"

type AttachmentState = "idle" | "uploading" | "processing" | "error" | "done"