# Preview

A link preview that shows a hover card on desktop and a drawer on mobile.

Preview is an open-source pattern 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.

Source: https://ui.suiss.app/docs/patterns/preview

**React**

**Example**

```tsx
import { CalendarBlankIcon } from "@phosphor-icons/react"
import {
  Preview,
  PreviewContent,
  PreviewTrigger,
} from "@/components/patterns/preview"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"

function ProfileCard() {
  return (
    <div className="flex gap-4">
      <Avatar size="lg">
        <AvatarImage src="https://github.com/vercel.png" alt="@nextjs" />
        <AvatarFallback>NJ</AvatarFallback>
      </Avatar>
      <div className="flex flex-col gap-1">
        <h4 className="text-sm font-semibold">@nextjs</h4>
        <p className="text-sm">
          The React framework, created and maintained by @vercel.
        </p>
        <div className="mt-1 flex items-center gap-1.5 text-xs text-muted-foreground">
          <CalendarBlankIcon />
          Joined December 2021
        </div>
      </div>
    </div>
  )
}

export default function PreviewDemo() {
  return (
    <div className="flex min-h-svh items-center justify-center p-4">
      <Preview>
        <PreviewTrigger href="#">@nextjs</PreviewTrigger>
        <PreviewContent>
          <ProfileCard />
        </PreviewContent>
      </Preview>
    </div>
  )
}
```

`Preview` reveals extra information about a link, such as a profile or page summary, without navigating. On desktop it renders a suiss/ui `HoverCard` that opens when the trigger is hovered or focused. On mobile (below `48rem`, via `useIsMobile`), where hover is not available, it renders a `Drawer` with a swipe handle that opens when the trigger is tapped. The drawer floats inset from the screen edges by default.

Use it for supplementary content that people can skip. Keep essential information and actions out of the preview, because the hover card opens only on hover or focus.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/preview
```

Works in any project with a `components.json`. See [Installation](https://ui.suiss.app/docs/installation#add-components) for the one-line registry setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add patterns preview
```

### Manual

1. Add the [`cn` helper](https://ui.suiss.app/docs/installation#the-cn-helper).

2. Add the pieces it builds on: [Drawer](https://ui.suiss.app/docs/components/drawer), [Hover Card](https://ui.suiss.app/docs/components/hover-card), `use-mobile`.

3. Copy the source into `components/patterns/preview.tsx`:

```tsx title="components/patterns/preview.tsx"
"use client"

import * as React from "react"
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"
import {
  HoverCard,
  HoverCardContent,
  HoverCardTrigger,
} from "@/components/ui/hover-card"
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"

type PreviewContextProps = {
  isMobile: boolean
}

const PreviewContext = React.createContext<PreviewContextProps | null>(null)

function usePreview() {
  const context = React.useContext(PreviewContext)

  if (!context) {
    throw new Error("usePreview must be used within a <Preview />")
  }

  return context
}

function Preview({
  open,
  defaultOpen,
  onOpenChange,
  floating = true,
  children,
}: Pick<
  React.ComponentProps<typeof HoverCard>,
  "open" | "defaultOpen" | "children"
> & {
  onOpenChange?: (open: boolean) => void
  floating?: boolean
}) {
  const isMobile = useIsMobile()
  const rootProps = {
    open,
    defaultOpen,
    onOpenChange: (next: boolean) => onOpenChange?.(next),
    children,
  }

  return (
    <PreviewContext.Provider value={{ isMobile }}>
      {isMobile ? (
        <Drawer
          data-slot="preview"
          floating={floating}
          showSwipeHandle
          {...rootProps}
        />
      ) : (
        <HoverCard data-slot="preview" {...rootProps} />
      )}
    </PreviewContext.Provider>
  )
}

function PreviewTrigger({
  href,
  className,
  ...props
}: React.HTMLAttributes<HTMLElement> & {
  href?: string
}) {
  const { isMobile } = usePreview()

  if (isMobile) {
    return (
      <DrawerTrigger
        data-slot="preview-trigger"
        className={cn(
          "cursor-pointer text-primary underline-offset-4 outline-none focus-visible:ring-3 focus-visible:ring-ring/50",
          className
        )}
        {...props}
      />
    )
  }

  return (
    <HoverCardTrigger
      data-slot="preview-trigger"
      href={href}
      className={cn(
        "text-primary underline-offset-4 outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring/50",
        className
      )}
      {...props}
    />
  )
}

function PreviewContent({
  align,
  alignOffset,
  side,
  sideOffset,
  showCloseButton,
  closeLabel,
  className,
  children,
  ...props
}: React.HTMLAttributes<HTMLElement> &
  Pick<
    React.ComponentProps<typeof HoverCardContent>,
    "align" | "alignOffset" | "side" | "sideOffset"
  > & {
    showCloseButton?: boolean
    closeLabel?: string
  }) {
  const { isMobile } = usePreview()

  if (isMobile) {
    return (
      <DrawerContent
        data-slot="preview-content"
        showCloseButton={showCloseButton}
        closeLabel={closeLabel}
        {...props}
      >
        <div
          className={cn(
            "p-4",
            showCloseButton && "pe-14",
            showCloseButton && closeLabel && "pe-24",
            className
          )}
        >
          {children}
        </div>
      </DrawerContent>
    )
  }

  return (
    <HoverCardContent
      data-slot="preview-content"
      align={align}
      alignOffset={alignOffset}
      side={side}
      sideOffset={sideOffset}
      className={className}
      {...props}
    >
      {children}
    </HoverCardContent>
  )
}

export { Preview, PreviewContent, PreviewTrigger }
```

4. Update the import paths to match your project.

## Usage

```tsx
import { Preview, PreviewTrigger, PreviewContent } from "@/components/patterns/preview"
```

```tsx
<div className="flex min-h-svh items-center justify-center p-4">
  <Preview>
    <PreviewTrigger href="#">@nextjs</PreviewTrigger>
    <PreviewContent>
      <ProfileCard />
    </PreviewContent>
  </Preview>
</div>
```

- Place `PreviewTrigger` and `PreviewContent` inside `Preview`. The parts throw when used outside it.
- Open state is uncontrolled by default; use `defaultOpen`, or `open` with `onOpenChange` to control it.
- On desktop the trigger is a link that uses `href`; on mobile it is a button that opens the drawer, so the link target is not followed.
- `align`, `alignOffset`, `side` and `sideOffset` position the desktop card. `showCloseButton` and `closeLabel` apply only to the mobile drawer.

## Examples

### With close button

Adds a close button to the mobile drawer with `showCloseButton`.

**Example**

```tsx
import { CalendarBlankIcon } from "@phosphor-icons/react"
import {
  Preview,
  PreviewContent,
  PreviewTrigger,
} from "@/components/patterns/preview"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"

function ProfileCard() {
  return (
    <div className="flex gap-4">
      <Avatar size="lg">
        <AvatarImage src="https://github.com/vercel.png" alt="@nextjs" />
        <AvatarFallback>NJ</AvatarFallback>
      </Avatar>
      <div className="flex flex-col gap-1">
        <h4 className="text-sm font-semibold">@nextjs</h4>
        <p className="text-sm">
          The React framework, created and maintained by @vercel.
        </p>
        <div className="mt-1 flex items-center gap-1.5 text-xs text-muted-foreground">
          <CalendarBlankIcon />
          Joined December 2021
        </div>
      </div>
    </div>
  )
}

export default function PreviewWithCloseButton() {
  return (
    <div className="flex min-h-svh items-center justify-center p-4">
      <Preview>
        <PreviewTrigger href="#">@nextjs</PreviewTrigger>
        <PreviewContent showCloseButton>
          <ProfileCard />
        </PreviewContent>
      </Preview>
    </div>
  )
}
```

### With close label

Uses a Done text button for the mobile close button through `closeLabel`.

**Example**

```tsx
import { CalendarBlankIcon } from "@phosphor-icons/react"
import {
  Preview,
  PreviewContent,
  PreviewTrigger,
} from "@/components/patterns/preview"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"

function ProfileCard() {
  return (
    <div className="flex gap-4">
      <Avatar size="lg">
        <AvatarImage src="https://github.com/vercel.png" alt="@nextjs" />
        <AvatarFallback>NJ</AvatarFallback>
      </Avatar>
      <div className="flex flex-col gap-1">
        <h4 className="text-sm font-semibold">@nextjs</h4>
        <p className="text-sm">
          The React framework, created and maintained by @vercel.
        </p>
        <div className="mt-1 flex items-center gap-1.5 text-xs text-muted-foreground">
          <CalendarBlankIcon />
          Joined December 2021
        </div>
      </div>
    </div>
  )
}

export default function PreviewWithCloseLabel() {
  return (
    <div className="flex min-h-svh items-center justify-center p-4">
      <Preview>
        <PreviewTrigger href="#">@nextjs</PreviewTrigger>
        <PreviewContent showCloseButton closeLabel="Done">
          <ProfileCard />
        </PreviewContent>
      </Preview>
    </div>
  )
}
```

### Attached

Sets `floating` to `false` so the mobile drawer sits on the screen edges.

**Example**

```tsx
import { CalendarBlankIcon } from "@phosphor-icons/react"
import {
  Preview,
  PreviewContent,
  PreviewTrigger,
} from "@/components/patterns/preview"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"

function ProfileCard() {
  return (
    <div className="flex gap-4">
      <Avatar size="lg">
        <AvatarImage src="https://github.com/vercel.png" alt="@nextjs" />
        <AvatarFallback>NJ</AvatarFallback>
      </Avatar>
      <div className="flex flex-col gap-1">
        <h4 className="text-sm font-semibold">@nextjs</h4>
        <p className="text-sm">
          The React framework, created and maintained by @vercel.
        </p>
        <div className="mt-1 flex items-center gap-1.5 text-xs text-muted-foreground">
          <CalendarBlankIcon />
          Joined December 2021
        </div>
      </div>
    </div>
  )
}

export default function PreviewAttached() {
  return (
    <div className="flex min-h-svh items-center justify-center p-4">
      <Preview floating={false}>
        <PreviewTrigger href="#">@nextjs</PreviewTrigger>
        <PreviewContent>
          <ProfileCard />
        </PreviewContent>
      </Preview>
    </div>
  )
}
```

## API Reference

- `Preview`
- `PreviewTrigger`
- `PreviewContent`

### Preview

The root that chooses HoverCard or Drawer and holds the open state.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `onOpenChange` | `(open: boolean) => void` | - | Called with the new open state when the preview opens or closes. |
| `floating` | `boolean` | `true` | Insets the mobile drawer from the screen edges. Has no effect on desktop. |

Also accepts the props of `React.ComponentProps<typeof HoverCard>`.

### PreviewTrigger

The link-styled element that opens the preview.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `href` | `string` | - | The link destination on desktop. Ignored on mobile. |

Also accepts the props of `React.HTMLAttributes<HTMLElement>`.

### PreviewContent

The card or drawer that holds the preview.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `showCloseButton` | `boolean` | - | Shows a close button in the mobile drawer. |
| `closeLabel` | `string` | - | Text for the mobile close button in place of the icon. |

Also accepts the props of `React.HTMLAttributes<HTMLElement>`, `React.ComponentProps<typeof HoverCardContent>`.

## Accessibility

- On desktop the trigger is a link; the card opens on hover and on keyboard focus, and closes when the pointer leaves.
- On mobile the trigger is a button that opens the drawer.
- The trigger shows a focus ring when focused with the keyboard.

## Notes

Styling hooks: `data-slot="preview"`, `preview-trigger` and `preview-content`. On mobile, `className` on `PreviewContent` applies to an inner padded wrapper, which adds end padding to clear the close button when it is shown.

**React Native, SwiftUI, Jetpack Compose**

![Preview](https://ui.suiss.app/previews/patterns-preview--open-by-default.light.webp)

```tsx
import { CalendarBlankIcon } from "phosphor-react-native"
import { View } from "react-native"
import { Preview, PreviewContent, PreviewTrigger } from "@/components/patterns/preview"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"

function ProfileCard() {
  return (
    <View className="flex-row gap-4">
      <Avatar size="lg">
        <AvatarImage
          alt="Ada Lovelace"
          source={{ uri: "https://i.pravatar.cc/150?img=5" }}
        />
        <AvatarFallback>AL</AvatarFallback>
      </Avatar>
      <View className="flex-1 gap-1">
        <Text className="text-sm font-semibold">@ada</Text>
        <Text className="text-sm">
          Mathematician and writer, known for her work on the Analytical Engine.
        </Text>
        <View className="mt-1 flex-row items-center gap-1.5">
          <Icon
            as={CalendarBlankIcon}
            className="size-4 text-muted-foreground"
          />
          <Text className="text-xs text-muted-foreground">
            Joined December 2021
          </Text>
        </View>
      </View>
    </View>
  )
}

function PreviewDemo({
  defaultOpen,
  floating,
  showCloseButton,
  closeLabel,
}: {
  defaultOpen?: boolean
  floating?: boolean
  showCloseButton?: boolean
  closeLabel?: string
}) {
  return (
    <View className="flex-1 items-center justify-center">
      <Preview defaultOpen={defaultOpen} floating={floating}>
        <PreviewTrigger>@ada</PreviewTrigger>
        <PreviewContent
          showCloseButton={showCloseButton}
          closeLabel={closeLabel}
          accessibilityLabel="@ada"
        >
          <ProfileCard />
        </PreviewContent>
      </Preview>
    </View>
  )
}

export function PreviewOpenByDefaultExample() {
  return (
    <PreviewDemo defaultOpen />
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct PreviewOpenByDefaultExample: View {
  var body: some View {
    PreviewDemo(open: true)
  }
}

struct PreviewDemo: View {
  var showCloseButton = false
  var closeLabel: LocalizedStringKey?

  @State private var open: Bool

  init(open: Bool = false, showCloseButton: Bool = false, closeLabel: LocalizedStringKey? = nil) {
    _open = State(initialValue: open)
    self.showCloseButton = showCloseButton
    self.closeLabel = closeLabel
  }

  var body: some View {
    Button {
      open = true
    } label: {
      Text(verbatim: "@ada")
        .textStyle(.base)
        .foregroundStyle(EsuissColor.primary)
    }
    .buttonStyle(PreviewTriggerStyle())
    .preview(isPresented: $open) {
      PreviewContent(showCloseButton: showCloseButton, closeLabel: closeLabel) {
        PreviewProfileCard()
      }
      .accessibilityLabel(Text(verbatim: "@ada"))
    }
    .frame(maxWidth: .infinity)
  }
}

struct PreviewTriggerStyle: ButtonStyle {
  func makeBody(configuration: Configuration) -> some View {
    configuration.label
      .opacity(configuration.isPressed ? 0.8 : 1)
  }
}

struct PreviewProfileCard: View {
  var body: some View {
    HStack(alignment: .top, spacing: 16) {
      Avatar(url: URL(string: "https://i.pravatar.cc/150?img=5"), initials: "AL", size: .lg)
        .accessibilityLabel(Text(verbatim: "Ada Lovelace"))
      VStack(alignment: .leading, spacing: 4) {
        Text(verbatim: "@ada")
          .textStyle(.sm, weight: .semibold)
          .foregroundStyle(EsuissColor.foreground)
        Text("Mathematician and writer, known for her work on the Analytical Engine.")
          .textStyle(.sm)
          .foregroundStyle(EsuissColor.foreground)
          .fixedSize(horizontal: false, vertical: true)
        HStack(spacing: 6) {
          Ph.calendarBlank.regular
            .frame(width: 16, height: 16)
          Text("Joined December 2021")
            .textStyle(.xs)
        }
        .foregroundStyle(EsuissColor.mutedForeground)
        .padding(.top, 4)
      }
      .frame(maxWidth: .infinity, alignment: .leading)
    }
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.CalendarBlank
import com.esuiss.ui.components.Avatar
import com.esuiss.ui.components.AvatarFallback
import com.esuiss.ui.components.AvatarSize
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Text
import com.esuiss.ui.patterns.Preview
import com.esuiss.ui.patterns.PreviewContent
import com.esuiss.ui.patterns.PreviewTrigger
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography

@Composable
fun PreviewOpenByDefaultExample() {
    PreviewDemo(defaultOpen = true)
}

@Composable
fun PreviewProfileCard() {
    val colors = EsuissTheme.colors
    Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
        Avatar(size = AvatarSize.Lg, contentDescription = "Ada Lovelace") {
            AvatarFallback("AL")
        }
        Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
            Text("@ada", style = Typography.sm.copy(fontWeight = FontWeight.SemiBold))
            Text(
                "Mathematician and writer, known for her work on the Analytical Engine.",
                style = Typography.sm,
            )
            Row(
                modifier = Modifier.padding(top = 4.dp),
                horizontalArrangement = Arrangement.spacedBy(6.dp),
                verticalAlignment = Alignment.CenterVertically,
            ) {
                Icon(PhosphorIcons.Regular.CalendarBlank, contentDescription = null, size = 16.dp, tint = colors.mutedForeground)
                Text("Joined December 2021", style = Typography.xs.copy(color = colors.mutedForeground))
            }
        }
    }
}

@Composable
fun PreviewDemo(
    defaultOpen: Boolean = false,
    floating: Boolean = true,
    showCloseButton: Boolean = false,
    closeLabel: String? = null,
) {
    var open by rememberSaveable { mutableStateOf(defaultOpen) }
    Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
        Preview(open = open, onOpenChange = { open = it }, floating = floating) {
            PreviewTrigger("@ada")
            PreviewContent(
                modifier = Modifier.semantics { contentDescription = "@ada" },
                showCloseButton = showCloseButton,
                closeLabel = closeLabel,
            ) {
                PreviewProfileCard()
            }
        }
    }
}
```

`PreviewTrigger` renders inline in the primary color and fades when pressed. It opens a `Drawer` that floats above the screen edges by default and has a swipe handle.

Use it to show a summary of a person, place or item from a mention or link without leaving the screen.

## Installation

**React Native**

```bash title="Terminal"
npx shadcn@latest add @esuiss-rn/preview
```

See [Installation](https://ui.suiss.app/docs/installation) for the `components.json` setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add preview
```

**SwiftUI**

```bash title="Terminal"
npx @esuiss/ui@latest add preview --platform swift
```

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add preview --platform kotlin
```

The CLI also adds what it builds on: [Drawer](https://ui.suiss.app/docs/components/drawer), [Text](https://ui.suiss.app/docs/components/text).

### Manual

**React Native**

**Uniwind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install cn
```

2. Add the pieces it builds on: [Drawer](https://ui.suiss.app/docs/components/drawer), [Text](https://ui.suiss.app/docs/components/text), and the primitives `components/primitives/use-render.tsx`.

3. Copy the source into `components/patterns/preview.tsx`:

```tsx title="uniwind/components/patterns/preview.tsx"
import { cn } from "cn"
import type * as React from "react"
import { View } from "react-native"
import { resolveClassName } from "@/components/primitives/use-render"
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"
import { Text, TextClassContext } from "@/components/ui/text"

function Preview({
  open,
  defaultOpen,
  onOpenChange,
  floating = true,
  children,
}: {
  open?: boolean
  defaultOpen?: boolean
  onOpenChange?: (open: boolean) => void
  floating?: boolean
  children?: React.ReactNode
}) {
  return (
    <Drawer
      open={open}
      defaultOpen={defaultOpen}
      onOpenChange={(next) => onOpenChange?.(next)}
      floating={floating}
      showSwipeHandle
    >
      {children}
    </Drawer>
  )
}

function PreviewTrigger({
  className,
  children,
  ...props
}: React.ComponentProps<typeof DrawerTrigger>) {
  return (
    <TextClassContext.Provider value="text-primary">
      <DrawerTrigger
        className={(state) =>
          cn("self-start active:opacity-60", resolveClassName(className, state))
        }
        {...props}
      >
        {typeof children === "string" || typeof children === "number" ? (
          <Text>{children}</Text>
        ) : (
          children
        )}
      </DrawerTrigger>
    </TextClassContext.Provider>
  )
}

function PreviewContent({
  showCloseButton,
  closeLabel,
  className,
  children,
  ...props
}: Omit<React.ComponentProps<typeof DrawerContent>, "className"> & {
  className?: string
}) {
  return (
    <DrawerContent
      showCloseButton={showCloseButton}
      closeLabel={closeLabel}
      {...props}
    >
      <View
        className={cn(
          "p-4",
          showCloseButton && "pe-14",
          showCloseButton && closeLabel && "pe-24",
          className
        )}
      >
        {children}
      </View>
    </DrawerContent>
  )
}

export { Preview, PreviewContent, PreviewTrigger }
```

4. Update the import paths to match your project.

**NativeWind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install cn
```

2. Add the pieces it builds on: [Drawer](https://ui.suiss.app/docs/components/drawer), [Text](https://ui.suiss.app/docs/components/text), and the primitives `components/primitives/use-render.tsx`.

3. Copy the source into `components/patterns/preview.tsx`:

```tsx title="nativewind/components/patterns/preview.tsx"
import { cn } from "cn"
import type * as React from "react"
import { View } from "react-native"
import { resolveClassName } from "@/components/primitives/use-render"
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"
import { Text, TextClassContext } from "@/components/ui/text"

function Preview({
  open,
  defaultOpen,
  onOpenChange,
  floating = true,
  children,
}: {
  open?: boolean
  defaultOpen?: boolean
  onOpenChange?: (open: boolean) => void
  floating?: boolean
  children?: React.ReactNode
}) {
  return (
    <Drawer
      open={open}
      defaultOpen={defaultOpen}
      onOpenChange={(next) => onOpenChange?.(next)}
      floating={floating}
      showSwipeHandle
    >
      {children}
    </Drawer>
  )
}

function PreviewTrigger({
  className,
  children,
  ...props
}: React.ComponentProps<typeof DrawerTrigger>) {
  return (
    <TextClassContext.Provider value="text-primary">
      <DrawerTrigger
        className={(state) =>
          cn("self-start active:opacity-60", resolveClassName(className, state))
        }
        {...props}
      >
        {typeof children === "string" || typeof children === "number" ? (
          <Text>{children}</Text>
        ) : (
          children
        )}
      </DrawerTrigger>
    </TextClassContext.Provider>
  )
}

function PreviewContent({
  showCloseButton,
  closeLabel,
  className,
  children,
  ...props
}: Omit<React.ComponentProps<typeof DrawerContent>, "className"> & {
  className?: string
}) {
  return (
    <DrawerContent
      showCloseButton={showCloseButton}
      closeLabel={closeLabel}
      {...props}
    >
      <View
        className={cn(
          "p-4",
          showCloseButton && "pe-14",
          showCloseButton && closeLabel && "pe-24",
          className
        )}
      >
        {children}
      </View>
    </DrawerContent>
  )
}

export { Preview, PreviewContent, PreviewTrigger }
```

4. Update the import paths to match your project.

**StyleSheet**

1. Add the pieces it builds on: [Drawer](https://ui.suiss.app/docs/components/drawer), [Text](https://ui.suiss.app/docs/components/text), and the primitives `components/primitives/use-render.tsx`.

2. Copy the source into `components/patterns/preview.tsx`:

```tsx title="stylesheet/components/patterns/preview.tsx"
import type * as React from "react"
import { Pressable, StyleSheet, View, type ViewStyle } from "react-native"
import { resolveStyle } from "@/components/primitives/use-render"
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"
import { Text, TextStyleContext } from "@/components/ui/text"
import { fontSize, useTheme } from "@/components/ui/theme"

const styles = StyleSheet.create({
  trigger: { alignSelf: "flex-start" },
  pressed: { opacity: 0.6 },
  body: { padding: 16 },
  bodyClose: { paddingEnd: 56 },
  bodyCloseLabel: { paddingEnd: 96 },
})

function Preview({
  open,
  defaultOpen,
  onOpenChange,
  floating = true,
  children,
}: {
  open?: boolean
  defaultOpen?: boolean
  onOpenChange?: (open: boolean) => void
  floating?: boolean
  children?: React.ReactNode
}) {
  return (
    <Drawer
      open={open}
      defaultOpen={defaultOpen}
      onOpenChange={(next) => onOpenChange?.(next)}
      floating={floating}
      showSwipeHandle
    >
      {children}
    </Drawer>
  )
}

function PreviewTrigger({
  style,
  children,
  ...props
}: React.ComponentProps<typeof DrawerTrigger>) {
  const { colors } = useTheme()
  return (
    <TextStyleContext.Provider
      value={{ ...fontSize.base, color: colors.primary }}
    >
      <DrawerTrigger
        style={(state) => [
          styles.trigger,
          resolveStyle(style, state) as ViewStyle,
        ]}
        render={(renderProps) => {
          const { style: baseStyle, ...rest } = renderProps
          return (
            <Pressable
              {...rest}
              style={({ pressed }) => [
                baseStyle as ViewStyle,
                pressed && styles.pressed,
              ]}
            />
          )
        }}
        {...props}
      >
        {typeof children === "string" || typeof children === "number" ? (
          <Text>{children}</Text>
        ) : (
          children
        )}
      </DrawerTrigger>
    </TextStyleContext.Provider>
  )
}

function PreviewContent({
  showCloseButton,
  closeLabel,
  style,
  children,
  ...props
}: Omit<React.ComponentProps<typeof DrawerContent>, "style"> & {
  style?: ViewStyle
}) {
  return (
    <DrawerContent
      showCloseButton={showCloseButton}
      closeLabel={closeLabel}
      {...props}
    >
      <View
        style={[
          styles.body,
          showCloseButton && styles.bodyClose,
          showCloseButton && closeLabel !== undefined && styles.bodyCloseLabel,
          style,
        ]}
      >
        {children}
      </View>
    </DrawerContent>
  )
}

export { Preview, PreviewContent, PreviewTrigger }
```

3. Update the import paths to match your project.

**SwiftUI**

1. Target iOS 17 or later.

2. Add the Swift package to your app:

```swift title="Package.swift"
.package(url: "https://github.com/phosphor-icons/swift", from: "2.1.0")
```

3. Copy the theme files every view uses:

- `swift/Theme/Theme.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Theme.swift)

- `swift/Theme/Tokens.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Tokens.swift)

4. Copy the source and every file it uses:

```swift title="swift/Patterns/Preview.swift"
import SwiftUI

extension View {
  public func preview<Content: View>(
    isPresented: Binding<Bool>,
    arrowEdge: Edge = .top,
    @ViewBuilder content: @escaping () -> Content
  ) -> some View {
    modifier(PreviewPresenter(isPresented: isPresented, arrowEdge: arrowEdge, content: content))
  }
}

struct PreviewPresenter<Body: View>: ViewModifier {
  let isPresented: Binding<Bool>
  let arrowEdge: Edge
  @ViewBuilder let content: () -> Body

  @CompactLayout private var compact

  func body(content host: Content) -> some View {
    let binding = isPresented
    host
      .drawer(isPresented: binding.gated(compact)) {
        content().environment(\.patternCompact, true)
      }
      .popover(isPresented: binding.gated(!compact), arrowEdge: arrowEdge) {
        content()
          .environment(\.patternCompact, false)
          .environment(\.overlayDismiss, OverlayDismiss { binding.wrappedValue = false })
      }
  }
}

public struct PreviewContent<Content: View>: View {
  let showCloseButton: Bool
  let closeLabel: LocalizedStringKey?
  let width: CGFloat?
  let content: Content

  @CompactLayout private var compact

  public init(
    showCloseButton: Bool = false,
    closeLabel: LocalizedStringKey? = nil,
    width: CGFloat? = 256,
    @ViewBuilder content: () -> Content
  ) {
    self.showCloseButton = showCloseButton
    self.closeLabel = closeLabel
    self.width = width
    self.content = content()
  }

  public var body: some View {
    if compact {
      DrawerContent(showCloseButton: showCloseButton, closeLabel: closeLabel) {
        VStack(alignment: .leading, spacing: 16) {
          content
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(16)
        .padding(.trailing, showCloseButton ? (closeLabel == nil ? 40 : 80) : 0)
      }
    } else {
      PopoverContent(width: width) { content }
    }
  }
}
```

- `swift/Components/Button.swift`: see [Button](https://ui.suiss.app/docs/components/button.md)

- `swift/Components/ButtonGroup.swift`: see [Button Group](https://ui.suiss.app/docs/components/button-group.md)

- `swift/Components/Dialog.swift`: see [Dialog](https://ui.suiss.app/docs/components/dialog.md)

- `swift/Components/Drawer.swift`: see [Drawer](https://ui.suiss.app/docs/components/drawer.md)

- `swift/Components/Overlay.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Components/Overlay.swift)

- `swift/Components/Popover.swift`: see [Popover](https://ui.suiss.app/docs/components/popover.md)

- `swift/Components/TextVariant.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Components/TextVariant.swift)

- `swift/Patterns/Adaptive.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Patterns/Adaptive.swift)

5. Add the files to your app target.

**Jetpack Compose**

1. Set `minSdk` to 24 or higher and add the dependencies:

```kotlin title="build.gradle.kts"
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
implementation("com.adamglin:phosphor-icon-android:1.0.0")
```

2. Copy the theme files every view uses:

- `kotlin/theme/Theme.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Theme.kt)

- `kotlin/theme/Tokens.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Tokens.kt)

3. Copy the source and every file it uses:

```kt title="kotlin/patterns/Preview.kt"
package com.esuiss.ui.patterns

import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.LocalContentColor
import com.esuiss.ui.components.LocalContentStyle
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography

private val LocalPreview = drawerPatternLocal()

@Composable
fun Preview(
    open: Boolean,
    onOpenChange: (Boolean) -> Unit,
    floating: Boolean = true,
    content: @Composable () -> Unit,
) {
    CompositionLocalProvider(
        LocalPreview provides DrawerPatternState(open, onOpenChange, floating),
        content = content,
    )
}

@Composable
fun PreviewTrigger(
    modifier: Modifier = Modifier,
    content: @Composable BoxScope.() -> Unit,
) {
    val state = LocalPreview.current
    val colors = EsuissTheme.colors
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    CompositionLocalProvider(
        LocalContentStyle provides LocalContentStyle.current.copy(
            color = colors.primary,
            textDecoration = if (pressed) TextDecoration.Underline else null,
        ),
        LocalContentColor provides colors.primary,
    ) {
        Box(
            modifier = modifier.clickable(
                interactionSource = interaction,
                indication = null,
                role = Role.Button,
            ) { state?.onOpenChange?.invoke(true) },
            content = content,
        )
    }
}

@Composable
fun PreviewTrigger(text: String, modifier: Modifier = Modifier) {
    PreviewTrigger(modifier) {
        BasicText(text, style = Typography.base.merge(LocalContentStyle.current))
    }
}

@Composable
fun PreviewContent(
    modifier: Modifier = Modifier,
    showCloseButton: Boolean = false,
    closeLabel: String? = null,
    content: @Composable ColumnScope.() -> Unit,
) {
    DrawerPatternContent(LocalPreview.current, modifier, showCloseButton, closeLabel) {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .padding(
                    start = 16.dp,
                    top = 16.dp,
                    bottom = 16.dp,
                    end = when {
                        showCloseButton && closeLabel != null -> 96.dp
                        showCloseButton -> 56.dp
                        else -> 16.dp
                    },
                ),
            content = content,
        )
    }
}
```

- `kotlin/components/Button.kt`: see [Button](https://ui.suiss.app/docs/components/button.md)

- `kotlin/components/ButtonGroup.kt`: see [Button Group](https://ui.suiss.app/docs/components/button-group.md)

- `kotlin/components/Content.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/Content.kt)

- `kotlin/components/Drawer.kt`: see [Drawer](https://ui.suiss.app/docs/components/drawer.md)

- `kotlin/components/Icon.kt`: see [Icon](https://ui.suiss.app/docs/components/icon.md)

- `kotlin/components/Overlay.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/Overlay.kt)

- `kotlin/patterns/DrawerPatterns.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/patterns/DrawerPatterns.kt)

4. Update the `com.esuiss.ui` package names to match your project.

## Usage

```tsx
import { Preview, PreviewTrigger, PreviewContent } from "@/components/patterns/preview"
```

```tsx
<PreviewDemo defaultOpen />
```

Put a `PreviewTrigger` and a `PreviewContent` inside `Preview`. A string or number child of the trigger is wrapped in `Text`.

The preview can be uncontrolled or controlled with `open`, `defaultOpen` and `onOpenChange`. `PreviewContent` pads its children and adds end padding when a close button is shown, so content does not sit under it.

## Examples

### Default

![Preview](https://ui.suiss.app/previews/patterns-preview--default.light.webp)

```tsx
import { CalendarBlankIcon } from "phosphor-react-native"
import { View } from "react-native"
import { Preview, PreviewContent, PreviewTrigger } from "@/components/patterns/preview"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"

function ProfileCard() {
  return (
    <View className="flex-row gap-4">
      <Avatar size="lg">
        <AvatarImage
          alt="Ada Lovelace"
          source={{ uri: "https://i.pravatar.cc/150?img=5" }}
        />
        <AvatarFallback>AL</AvatarFallback>
      </Avatar>
      <View className="flex-1 gap-1">
        <Text className="text-sm font-semibold">@ada</Text>
        <Text className="text-sm">
          Mathematician and writer, known for her work on the Analytical Engine.
        </Text>
        <View className="mt-1 flex-row items-center gap-1.5">
          <Icon
            as={CalendarBlankIcon}
            className="size-4 text-muted-foreground"
          />
          <Text className="text-xs text-muted-foreground">
            Joined December 2021
          </Text>
        </View>
      </View>
    </View>
  )
}

function PreviewDemo({
  defaultOpen,
  floating,
  showCloseButton,
  closeLabel,
}: {
  defaultOpen?: boolean
  floating?: boolean
  showCloseButton?: boolean
  closeLabel?: string
}) {
  return (
    <View className="flex-1 items-center justify-center">
      <Preview defaultOpen={defaultOpen} floating={floating}>
        <PreviewTrigger>@ada</PreviewTrigger>
        <PreviewContent
          showCloseButton={showCloseButton}
          closeLabel={closeLabel}
          accessibilityLabel="@ada"
        >
          <ProfileCard />
        </PreviewContent>
      </Preview>
    </View>
  )
}

export function PreviewExample() {
  return (
    <PreviewDemo />
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct PreviewDemo: View {
  var showCloseButton = false
  var closeLabel: LocalizedStringKey?

  @State private var open: Bool

  init(open: Bool = false, showCloseButton: Bool = false, closeLabel: LocalizedStringKey? = nil) {
    _open = State(initialValue: open)
    self.showCloseButton = showCloseButton
    self.closeLabel = closeLabel
  }

  var body: some View {
    Button {
      open = true
    } label: {
      Text(verbatim: "@ada")
        .textStyle(.base)
        .foregroundStyle(EsuissColor.primary)
    }
    .buttonStyle(PreviewTriggerStyle())
    .preview(isPresented: $open) {
      PreviewContent(showCloseButton: showCloseButton, closeLabel: closeLabel) {
        PreviewProfileCard()
      }
      .accessibilityLabel(Text(verbatim: "@ada"))
    }
    .frame(maxWidth: .infinity)
  }
}

struct PreviewTriggerStyle: ButtonStyle {
  func makeBody(configuration: Configuration) -> some View {
    configuration.label
      .opacity(configuration.isPressed ? 0.8 : 1)
  }
}

struct PreviewProfileCard: View {
  var body: some View {
    HStack(alignment: .top, spacing: 16) {
      Avatar(url: URL(string: "https://i.pravatar.cc/150?img=5"), initials: "AL", size: .lg)
        .accessibilityLabel(Text(verbatim: "Ada Lovelace"))
      VStack(alignment: .leading, spacing: 4) {
        Text(verbatim: "@ada")
          .textStyle(.sm, weight: .semibold)
          .foregroundStyle(EsuissColor.foreground)
        Text("Mathematician and writer, known for her work on the Analytical Engine.")
          .textStyle(.sm)
          .foregroundStyle(EsuissColor.foreground)
          .fixedSize(horizontal: false, vertical: true)
        HStack(spacing: 6) {
          Ph.calendarBlank.regular
            .frame(width: 16, height: 16)
          Text("Joined December 2021")
            .textStyle(.xs)
        }
        .foregroundStyle(EsuissColor.mutedForeground)
        .padding(.top, 4)
      }
      .frame(maxWidth: .infinity, alignment: .leading)
    }
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.CalendarBlank
import com.esuiss.ui.components.Avatar
import com.esuiss.ui.components.AvatarFallback
import com.esuiss.ui.components.AvatarSize
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Text
import com.esuiss.ui.patterns.Preview
import com.esuiss.ui.patterns.PreviewContent
import com.esuiss.ui.patterns.PreviewTrigger
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography

@Composable
fun PreviewProfileCard() {
    val colors = EsuissTheme.colors
    Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
        Avatar(size = AvatarSize.Lg, contentDescription = "Ada Lovelace") {
            AvatarFallback("AL")
        }
        Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
            Text("@ada", style = Typography.sm.copy(fontWeight = FontWeight.SemiBold))
            Text(
                "Mathematician and writer, known for her work on the Analytical Engine.",
                style = Typography.sm,
            )
            Row(
                modifier = Modifier.padding(top = 4.dp),
                horizontalArrangement = Arrangement.spacedBy(6.dp),
                verticalAlignment = Alignment.CenterVertically,
            ) {
                Icon(PhosphorIcons.Regular.CalendarBlank, contentDescription = null, size = 16.dp, tint = colors.mutedForeground)
                Text("Joined December 2021", style = Typography.xs.copy(color = colors.mutedForeground))
            }
        }
    }
}

@Composable
fun PreviewDemo(
    defaultOpen: Boolean = false,
    floating: Boolean = true,
    showCloseButton: Boolean = false,
    closeLabel: String? = null,
) {
    var open by rememberSaveable { mutableStateOf(defaultOpen) }
    Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
        Preview(open = open, onOpenChange = { open = it }, floating = floating) {
            PreviewTrigger("@ada")
            PreviewContent(
                modifier = Modifier.semantics { contentDescription = "@ada" },
                showCloseButton = showCloseButton,
                closeLabel = closeLabel,
            ) {
                PreviewProfileCard()
            }
        }
    }
}
```

### With close button

Shows a close button with `showCloseButton`.

![Preview](https://ui.suiss.app/previews/patterns-preview--with-close-button.light.webp)

```tsx
import { CalendarBlankIcon } from "phosphor-react-native"
import { View } from "react-native"
import { Preview, PreviewContent, PreviewTrigger } from "@/components/patterns/preview"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"

function ProfileCard() {
  return (
    <View className="flex-row gap-4">
      <Avatar size="lg">
        <AvatarImage
          alt="Ada Lovelace"
          source={{ uri: "https://i.pravatar.cc/150?img=5" }}
        />
        <AvatarFallback>AL</AvatarFallback>
      </Avatar>
      <View className="flex-1 gap-1">
        <Text className="text-sm font-semibold">@ada</Text>
        <Text className="text-sm">
          Mathematician and writer, known for her work on the Analytical Engine.
        </Text>
        <View className="mt-1 flex-row items-center gap-1.5">
          <Icon
            as={CalendarBlankIcon}
            className="size-4 text-muted-foreground"
          />
          <Text className="text-xs text-muted-foreground">
            Joined December 2021
          </Text>
        </View>
      </View>
    </View>
  )
}

function PreviewDemo({
  defaultOpen,
  floating,
  showCloseButton,
  closeLabel,
}: {
  defaultOpen?: boolean
  floating?: boolean
  showCloseButton?: boolean
  closeLabel?: string
}) {
  return (
    <View className="flex-1 items-center justify-center">
      <Preview defaultOpen={defaultOpen} floating={floating}>
        <PreviewTrigger>@ada</PreviewTrigger>
        <PreviewContent
          showCloseButton={showCloseButton}
          closeLabel={closeLabel}
          accessibilityLabel="@ada"
        >
          <ProfileCard />
        </PreviewContent>
      </Preview>
    </View>
  )
}

export function PreviewWithCloseButtonExample() {
  return (
    <PreviewDemo defaultOpen showCloseButton />
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct PreviewWithCloseButtonExample: View {
  var body: some View {
    PreviewDemo(open: true, showCloseButton: true)
  }
}

struct PreviewDemo: View {
  var showCloseButton = false
  var closeLabel: LocalizedStringKey?

  @State private var open: Bool

  init(open: Bool = false, showCloseButton: Bool = false, closeLabel: LocalizedStringKey? = nil) {
    _open = State(initialValue: open)
    self.showCloseButton = showCloseButton
    self.closeLabel = closeLabel
  }

  var body: some View {
    Button {
      open = true
    } label: {
      Text(verbatim: "@ada")
        .textStyle(.base)
        .foregroundStyle(EsuissColor.primary)
    }
    .buttonStyle(PreviewTriggerStyle())
    .preview(isPresented: $open) {
      PreviewContent(showCloseButton: showCloseButton, closeLabel: closeLabel) {
        PreviewProfileCard()
      }
      .accessibilityLabel(Text(verbatim: "@ada"))
    }
    .frame(maxWidth: .infinity)
  }
}

struct PreviewTriggerStyle: ButtonStyle {
  func makeBody(configuration: Configuration) -> some View {
    configuration.label
      .opacity(configuration.isPressed ? 0.8 : 1)
  }
}

struct PreviewProfileCard: View {
  var body: some View {
    HStack(alignment: .top, spacing: 16) {
      Avatar(url: URL(string: "https://i.pravatar.cc/150?img=5"), initials: "AL", size: .lg)
        .accessibilityLabel(Text(verbatim: "Ada Lovelace"))
      VStack(alignment: .leading, spacing: 4) {
        Text(verbatim: "@ada")
          .textStyle(.sm, weight: .semibold)
          .foregroundStyle(EsuissColor.foreground)
        Text("Mathematician and writer, known for her work on the Analytical Engine.")
          .textStyle(.sm)
          .foregroundStyle(EsuissColor.foreground)
          .fixedSize(horizontal: false, vertical: true)
        HStack(spacing: 6) {
          Ph.calendarBlank.regular
            .frame(width: 16, height: 16)
          Text("Joined December 2021")
            .textStyle(.xs)
        }
        .foregroundStyle(EsuissColor.mutedForeground)
        .padding(.top, 4)
      }
      .frame(maxWidth: .infinity, alignment: .leading)
    }
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.CalendarBlank
import com.esuiss.ui.components.Avatar
import com.esuiss.ui.components.AvatarFallback
import com.esuiss.ui.components.AvatarSize
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Text
import com.esuiss.ui.patterns.Preview
import com.esuiss.ui.patterns.PreviewContent
import com.esuiss.ui.patterns.PreviewTrigger
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography

@Composable
fun PreviewWithCloseButtonExample() {
    PreviewDemo(defaultOpen = true, showCloseButton = true)
}

@Composable
fun PreviewProfileCard() {
    val colors = EsuissTheme.colors
    Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
        Avatar(size = AvatarSize.Lg, contentDescription = "Ada Lovelace") {
            AvatarFallback("AL")
        }
        Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
            Text("@ada", style = Typography.sm.copy(fontWeight = FontWeight.SemiBold))
            Text(
                "Mathematician and writer, known for her work on the Analytical Engine.",
                style = Typography.sm,
            )
            Row(
                modifier = Modifier.padding(top = 4.dp),
                horizontalArrangement = Arrangement.spacedBy(6.dp),
                verticalAlignment = Alignment.CenterVertically,
            ) {
                Icon(PhosphorIcons.Regular.CalendarBlank, contentDescription = null, size = 16.dp, tint = colors.mutedForeground)
                Text("Joined December 2021", style = Typography.xs.copy(color = colors.mutedForeground))
            }
        }
    }
}

@Composable
fun PreviewDemo(
    defaultOpen: Boolean = false,
    floating: Boolean = true,
    showCloseButton: Boolean = false,
    closeLabel: String? = null,
) {
    var open by rememberSaveable { mutableStateOf(defaultOpen) }
    Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
        Preview(open = open, onOpenChange = { open = it }, floating = floating) {
            PreviewTrigger("@ada")
            PreviewContent(
                modifier = Modifier.semantics { contentDescription = "@ada" },
                showCloseButton = showCloseButton,
                closeLabel = closeLabel,
            ) {
                PreviewProfileCard()
            }
        }
    }
}
```

### With close label

Labels the close button `Done` with `closeLabel`.

![Preview](https://ui.suiss.app/previews/patterns-preview--with-close-label.light.webp)

```tsx
import { CalendarBlankIcon } from "phosphor-react-native"
import { View } from "react-native"
import { Preview, PreviewContent, PreviewTrigger } from "@/components/patterns/preview"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"

function ProfileCard() {
  return (
    <View className="flex-row gap-4">
      <Avatar size="lg">
        <AvatarImage
          alt="Ada Lovelace"
          source={{ uri: "https://i.pravatar.cc/150?img=5" }}
        />
        <AvatarFallback>AL</AvatarFallback>
      </Avatar>
      <View className="flex-1 gap-1">
        <Text className="text-sm font-semibold">@ada</Text>
        <Text className="text-sm">
          Mathematician and writer, known for her work on the Analytical Engine.
        </Text>
        <View className="mt-1 flex-row items-center gap-1.5">
          <Icon
            as={CalendarBlankIcon}
            className="size-4 text-muted-foreground"
          />
          <Text className="text-xs text-muted-foreground">
            Joined December 2021
          </Text>
        </View>
      </View>
    </View>
  )
}

function PreviewDemo({
  defaultOpen,
  floating,
  showCloseButton,
  closeLabel,
}: {
  defaultOpen?: boolean
  floating?: boolean
  showCloseButton?: boolean
  closeLabel?: string
}) {
  return (
    <View className="flex-1 items-center justify-center">
      <Preview defaultOpen={defaultOpen} floating={floating}>
        <PreviewTrigger>@ada</PreviewTrigger>
        <PreviewContent
          showCloseButton={showCloseButton}
          closeLabel={closeLabel}
          accessibilityLabel="@ada"
        >
          <ProfileCard />
        </PreviewContent>
      </Preview>
    </View>
  )
}

export function PreviewWithCloseLabelExample() {
  return (
    <PreviewDemo defaultOpen showCloseButton closeLabel="Done" />
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct PreviewWithCloseLabelExample: View {
  var body: some View {
    PreviewDemo(open: true, showCloseButton: true, closeLabel: "Done")
  }
}

struct PreviewDemo: View {
  var showCloseButton = false
  var closeLabel: LocalizedStringKey?

  @State private var open: Bool

  init(open: Bool = false, showCloseButton: Bool = false, closeLabel: LocalizedStringKey? = nil) {
    _open = State(initialValue: open)
    self.showCloseButton = showCloseButton
    self.closeLabel = closeLabel
  }

  var body: some View {
    Button {
      open = true
    } label: {
      Text(verbatim: "@ada")
        .textStyle(.base)
        .foregroundStyle(EsuissColor.primary)
    }
    .buttonStyle(PreviewTriggerStyle())
    .preview(isPresented: $open) {
      PreviewContent(showCloseButton: showCloseButton, closeLabel: closeLabel) {
        PreviewProfileCard()
      }
      .accessibilityLabel(Text(verbatim: "@ada"))
    }
    .frame(maxWidth: .infinity)
  }
}

struct PreviewTriggerStyle: ButtonStyle {
  func makeBody(configuration: Configuration) -> some View {
    configuration.label
      .opacity(configuration.isPressed ? 0.8 : 1)
  }
}

struct PreviewProfileCard: View {
  var body: some View {
    HStack(alignment: .top, spacing: 16) {
      Avatar(url: URL(string: "https://i.pravatar.cc/150?img=5"), initials: "AL", size: .lg)
        .accessibilityLabel(Text(verbatim: "Ada Lovelace"))
      VStack(alignment: .leading, spacing: 4) {
        Text(verbatim: "@ada")
          .textStyle(.sm, weight: .semibold)
          .foregroundStyle(EsuissColor.foreground)
        Text("Mathematician and writer, known for her work on the Analytical Engine.")
          .textStyle(.sm)
          .foregroundStyle(EsuissColor.foreground)
          .fixedSize(horizontal: false, vertical: true)
        HStack(spacing: 6) {
          Ph.calendarBlank.regular
            .frame(width: 16, height: 16)
          Text("Joined December 2021")
            .textStyle(.xs)
        }
        .foregroundStyle(EsuissColor.mutedForeground)
        .padding(.top, 4)
      }
      .frame(maxWidth: .infinity, alignment: .leading)
    }
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.CalendarBlank
import com.esuiss.ui.components.Avatar
import com.esuiss.ui.components.AvatarFallback
import com.esuiss.ui.components.AvatarSize
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Text
import com.esuiss.ui.patterns.Preview
import com.esuiss.ui.patterns.PreviewContent
import com.esuiss.ui.patterns.PreviewTrigger
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography

@Composable
fun PreviewWithCloseLabelExample() {
    PreviewDemo(defaultOpen = true, showCloseButton = true, closeLabel = "Done")
}

@Composable
fun PreviewProfileCard() {
    val colors = EsuissTheme.colors
    Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
        Avatar(size = AvatarSize.Lg, contentDescription = "Ada Lovelace") {
            AvatarFallback("AL")
        }
        Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
            Text("@ada", style = Typography.sm.copy(fontWeight = FontWeight.SemiBold))
            Text(
                "Mathematician and writer, known for her work on the Analytical Engine.",
                style = Typography.sm,
            )
            Row(
                modifier = Modifier.padding(top = 4.dp),
                horizontalArrangement = Arrangement.spacedBy(6.dp),
                verticalAlignment = Alignment.CenterVertically,
            ) {
                Icon(PhosphorIcons.Regular.CalendarBlank, contentDescription = null, size = 16.dp, tint = colors.mutedForeground)
                Text("Joined December 2021", style = Typography.xs.copy(color = colors.mutedForeground))
            }
        }
    }
}

@Composable
fun PreviewDemo(
    defaultOpen: Boolean = false,
    floating: Boolean = true,
    showCloseButton: Boolean = false,
    closeLabel: String? = null,
) {
    var open by rememberSaveable { mutableStateOf(defaultOpen) }
    Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
        Preview(open = open, onOpenChange = { open = it }, floating = floating) {
            PreviewTrigger("@ada")
            PreviewContent(
                modifier = Modifier.semantics { contentDescription = "@ada" },
                showCloseButton = showCloseButton,
                closeLabel = closeLabel,
            ) {
                PreviewProfileCard()
            }
        }
    }
}
```

**React Native, Jetpack Compose**

### Attached

Sets `floating` to `false` so the drawer sits on the screen edges.

![Preview](https://ui.suiss.app/previews/patterns-preview--attached.light.webp)

```tsx
import { CalendarBlankIcon } from "phosphor-react-native"
import { View } from "react-native"
import { Preview, PreviewContent, PreviewTrigger } from "@/components/patterns/preview"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"

function ProfileCard() {
  return (
    <View className="flex-row gap-4">
      <Avatar size="lg">
        <AvatarImage
          alt="Ada Lovelace"
          source={{ uri: "https://i.pravatar.cc/150?img=5" }}
        />
        <AvatarFallback>AL</AvatarFallback>
      </Avatar>
      <View className="flex-1 gap-1">
        <Text className="text-sm font-semibold">@ada</Text>
        <Text className="text-sm">
          Mathematician and writer, known for her work on the Analytical Engine.
        </Text>
        <View className="mt-1 flex-row items-center gap-1.5">
          <Icon
            as={CalendarBlankIcon}
            className="size-4 text-muted-foreground"
          />
          <Text className="text-xs text-muted-foreground">
            Joined December 2021
          </Text>
        </View>
      </View>
    </View>
  )
}

function PreviewDemo({
  defaultOpen,
  floating,
  showCloseButton,
  closeLabel,
}: {
  defaultOpen?: boolean
  floating?: boolean
  showCloseButton?: boolean
  closeLabel?: string
}) {
  return (
    <View className="flex-1 items-center justify-center">
      <Preview defaultOpen={defaultOpen} floating={floating}>
        <PreviewTrigger>@ada</PreviewTrigger>
        <PreviewContent
          showCloseButton={showCloseButton}
          closeLabel={closeLabel}
          accessibilityLabel="@ada"
        >
          <ProfileCard />
        </PreviewContent>
      </Preview>
    </View>
  )
}

export function PreviewAttachedExample() {
  return (
    <PreviewDemo defaultOpen floating={false} />
  )
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.CalendarBlank
import com.esuiss.ui.components.Avatar
import com.esuiss.ui.components.AvatarFallback
import com.esuiss.ui.components.AvatarSize
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Text
import com.esuiss.ui.patterns.Preview
import com.esuiss.ui.patterns.PreviewContent
import com.esuiss.ui.patterns.PreviewTrigger
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography

@Composable
fun PreviewAttachedExample() {
    PreviewDemo(defaultOpen = true, floating = false)
}

@Composable
fun PreviewProfileCard() {
    val colors = EsuissTheme.colors
    Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
        Avatar(size = AvatarSize.Lg, contentDescription = "Ada Lovelace") {
            AvatarFallback("AL")
        }
        Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
            Text("@ada", style = Typography.sm.copy(fontWeight = FontWeight.SemiBold))
            Text(
                "Mathematician and writer, known for her work on the Analytical Engine.",
                style = Typography.sm,
            )
            Row(
                modifier = Modifier.padding(top = 4.dp),
                horizontalArrangement = Arrangement.spacedBy(6.dp),
                verticalAlignment = Alignment.CenterVertically,
            ) {
                Icon(PhosphorIcons.Regular.CalendarBlank, contentDescription = null, size = 16.dp, tint = colors.mutedForeground)
                Text("Joined December 2021", style = Typography.xs.copy(color = colors.mutedForeground))
            }
        }
    }
}

@Composable
fun PreviewDemo(
    defaultOpen: Boolean = false,
    floating: Boolean = true,
    showCloseButton: Boolean = false,
    closeLabel: String? = null,
) {
    var open by rememberSaveable { mutableStateOf(defaultOpen) }
    Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
        Preview(open = open, onOpenChange = { open = it }, floating = floating) {
            PreviewTrigger("@ada")
            PreviewContent(
                modifier = Modifier.semantics { contentDescription = "@ada" },
                showCloseButton = showCloseButton,
                closeLabel = closeLabel,
            ) {
                PreviewProfileCard()
            }
        }
    }
}
```

## API Reference

### Preview

The root drawer, floating by default.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | - | Controls whether the preview is open. |
| `defaultOpen` | `boolean` | - | Whether the preview starts open when uncontrolled. |
| `onOpenChange` | `(open: boolean) => void` | - | Called when the preview opens or closes. |
| `floating` | `boolean` | `true` | Insets the drawer from the screen edges. Defaults to `true`. |
| `children` | `React.ReactNode` | - |  |

### PreviewTrigger

The link-styled element that opens the preview.

Also accepts the props of `React.ComponentProps<typeof DrawerTrigger>`.

### PreviewContent

The drawer content with padding for the preview.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - |  |

Also accepts the props of `React.ComponentProps<typeof DrawerContent>`.

## Accessibility

- Give `PreviewContent` an `accessibilityLabel` that names what is previewed.

## Notes

- The trigger uses the `text-primary` color.

