# Composition

How the components of ferry-ui go together with parts, asChild, slot props and state props.

ferry-ui has three layers of components. A primitive is one control or one container. A pattern puts primitives together for a frequent need. A layout component makes the frame of the app.

## Put parts together

Many components are a group of parts. Import the parts. Nest them. Leave out a part that you do not need.

```tsx
import { Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ferry-ui'

export default function Parts() {
  return (
    <Card className="w-full max-w-sm">
      <CardHeader>
        <div>
          <CardTitle>Payment method</CardTitle>
          <CardDescription>The charge date is the first day of each month.</CardDescription>
        </div>
        <CardAction>
          <Button size="tiny">Replace</Button>
        </CardAction>
      </CardHeader>
      <CardContent className="text-sm">Card ending in 4242</CardContent>
      <CardFooter className="justify-between text-[13px] text-foreground-light">Next charge on Nov 1</CardFooter>
    </Card>
  )
}
```

## Use asChild to keep your element

`DialogTrigger` renders a `<button>`. With `asChild`, it renders no element of its own. It gives its behavior to its child.

Use `asChild` to make a [Button](/docs/components/button) the trigger of a [Dialog](/docs/components/dialog), a menu or a popover.

```tsx
import {
  Button,
  Dialog,
  DialogBody,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
  Kbd,
} from 'ferry-ui'

export default function AsChildTrigger() {
  return (
    <Dialog>
      {/* The trigger adds its behavior to the Button. The page gets one <button>, not two. */}
      <DialogTrigger asChild>
        <Button>Keyboard shortcuts</Button>
      </DialogTrigger>
      <DialogContent size="sm">
        <DialogHeader>
          <DialogTitle>Keyboard shortcuts</DialogTitle>
          <DialogDescription>These keys work on each page.</DialogDescription>
        </DialogHeader>
        <DialogBody>
          <p className="flex items-center justify-between text-[13px] text-foreground-light">
            Close a dialog <Kbd>Esc</Kbd>
          </p>
        </DialogBody>
        <DialogFooter>
          <DialogClose asChild>
            <Button variant="primary">Done</Button>
          </DialogClose>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}
```

`Button` also accepts `asChild`. It then gives its look to its child, for example a link.

```tsx
import { Button } from 'ferry-ui'
import { ExternalLink } from 'lucide-react'

export default function AsChildLink() {
  return (
    // The Button gives its look to the link. The page gets an <a>, not a <button>.
    <Button asChild iconRight={<ExternalLink />}>
      <a href="https://github.com/Carter2307/ferry-ui" target="_blank" rel="noreferrer">
        Open the repository
      </a>
    </Button>
  )
}
```

## Fill a slot with a prop

A slot is a prop that accepts an element. The component puts the element in the correct place.

```tsx
import { Badge, ResourceCard, StatusLine } from 'ferry-ui'
import { FolderKanban } from 'lucide-react'

export default function Slots() {
  return (
    <ResourceCard
      as="div"
      className="w-full max-w-xs"
      name="Billing portal"
      icon={<FolderKanban />}
      subtitle="Owner: Maya Chen"
      badges={
        <Badge font="mono" shape="square">
          Pro
        </Badge>
      }
      footer={<StatusLine tone="success">The project is active</StatusLine>}
    />
  )
}
```

| Slot prop | Components |
| --- | --- |
| `icon` | `Button`, `EmptyState`, `ResourceCard` |
| `actions` | `PageHeader`, `EmptyState`, `Callout` |
| `footer` | `FormCard`, `ResourceCard`, `IconRail` |
| `trigger` | `ConfirmDialog`, `MobileNav` |
| `topBar`, `rail`, `mobileNav` | `AppShell` |

## Choose who holds the state

A component with a state has two modes. In the uncontrolled mode, the component holds the state. A prop such as `defaultValue` gives the first value.

```tsx
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'ferry-ui'

export default function Uncontrolled() {
  return (
    // `defaultValue` gives the first tab. After that, Tabs holds the state.
    <Tabs defaultValue="overview" className="w-full max-w-md">
      <TabsList aria-label="Project sections">
        <TabsTrigger value="overview">Overview</TabsTrigger>
        <TabsTrigger value="invoices">Invoices</TabsTrigger>
        <TabsTrigger value="members">Members</TabsTrigger>
      </TabsList>
      <TabsContent value="overview" className="text-[13px] text-foreground-light">
        Billing portal has 3 open invoices and 6 members.
      </TabsContent>
      <TabsContent value="invoices" className="text-[13px] text-foreground-light">
        INV-2041, INV-2042 and INV-2043 are open.
      </TabsContent>
      <TabsContent value="members" className="text-[13px] text-foreground-light">
        Maya Chen, Sam Lee and 4 more members.
      </TabsContent>
    </Tabs>
  )
}
```

In the controlled mode, your code holds the state. Pass the value and a function that gets each change.

```tsx
import * as React from 'react'
import { Button, Tabs, TabsContent, TabsList, TabsTrigger } from 'ferry-ui'

export default function Controlled() {
  // The state is in your code: you read it and you change it.
  const [tab, setTab] = React.useState('overview')

  return (
    <Tabs value={tab} onValueChange={setTab} className="w-full max-w-md">
      <TabsList aria-label="Project sections">
        <TabsTrigger value="overview">Overview</TabsTrigger>
        <TabsTrigger value="invoices">Invoices</TabsTrigger>
      </TabsList>
      <TabsContent value="overview" className="flex items-center justify-between gap-3 text-[13px] text-foreground-light">
        Billing portal has 3 open invoices.
        <Button size="tiny" onClick={() => setTab('invoices')}>
          Show the invoices
        </Button>
      </TabsContent>
      <TabsContent value="invoices" className="text-[13px] text-foreground-light">
        INV-2041, INV-2042 and INV-2043 are open.
      </TabsContent>
    </Tabs>
  )
}
```

| State | Controlled | Uncontrolled |
| --- | --- | --- |
| A value | `value` and `onValueChange` | `defaultValue` |
| Open or closed | `open` and `onOpenChange` | `defaultOpen` |
| Checked or not | `checked` and `onCheckedChange` | `defaultChecked` |
| Pressed or not | `pressed` and `onPressedChange` | `defaultPressed` |

ferry-ui has no store. It does not load data, and it does not know your router.

## Find a part with data-slot

Each component sets a `data-slot` attribute on its root element and on its important parts. The value is the name of the part: `dialog-title`, `card-footer`, `field-hint`.

Use the attribute to find a part in a test.

```ts title="invite-dialog.test.ts"
const title = document.querySelector('[data-slot="dialog-title"]')
```

Use it also to reach a part from CSS. In this demo, a class on `Field` aligns the hint to the right.

```tsx
import * as React from 'react'
import { Field, Textarea } from 'ferry-ui'

const LIMIT = 160

export default function DataSlot() {
  const [note, setNote] = React.useState('Send the invoice to the billing contact.')

  return (
    // Field has no class prop for its hint. The selector finds the hint by its data-slot.
    <Field
      label="Note on the invoice"
      hint={`${note.length} / ${LIMIT}`}
      className="w-full max-w-sm [&_[data-slot=field-hint]]:text-right"
    >
      <Textarea maxLength={LIMIT} value={note} onChange={(event) => setNote(event.target.value)} />
    </Field>
  )
}
```

## Props that most components accept

| Prop | What the component does |
| --- | --- |
| `className` | Adds it after its own classes. Your class wins. |
| `ref` | Accepts it as a regular prop. |
| Other props | Puts them on its root element: `id`, `aria-*`, `data-*`. |

<Callout tone="info" title="Some components have a closed list of props">
  A component that puts many parts together has no single root element. It accepts only the props that its page lists. Examples are [Confirm Dialog](/docs/components/confirm-dialog) and [Command Menu](/docs/components/command-menu).
</Callout>

## Next steps

- Read [Forms](/docs/handbook/forms) to put fields and actions together.
- Read [Routing](/docs/handbook/routing) to connect the links to your router.
- Open the page of [Dialog](/docs/components/dialog) to see a component with many parts.
