# Skill Detail

A skill's detail surface, in two columns: a rail that says what the skill is and a workspace that shows what is inside it. A card opens it, the dialog is optional.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. An agent skill is available at [/.well-known/agent-skills/site-skill.md](/.well-known/agent-skills/site-skill.md).





<ComponentPreview name="skill-detail" align="start" previewClassName="p-4 sm:p-6">
  <SkillDetailDemo />
</ComponentPreview>

## Installation [#installation]

```bash
npx shadcn@latest add https://motif-ui.vercel.app/r/skill-detail.json
```

## Usage [#usage]

```tsx
import { SkillCard, SkillDetailDialog } from "@/components/agents/skill-detail";

export function Library({ skills }: { skills: Skill[] }) {
  const [openId, setOpenId] = useState<string | null>(null);
  const open = skills.find((skill) => skill.id === openId);

  return (
    <>
      <div className="grid grid-cols-2 gap-4 sm:grid-cols-3">
        {skills.map((skill) => (
          <SkillCard
            cover={skill.cover}
            description={skill.summary}
            key={skill.id}
            onOpen={() => setOpenId(skill.id)}
            title={skill.name}
          />
        ))}
      </div>

      {open ? (
        <SkillDetailDialog
          author={open.author}
          cover={open.cover}
          description={open.description}
          files={open.files}
          folder={open.slug}
          onOpenChange={(next) => setOpenId(next ? open.id : null)}
          open
          prompts={open.prompts}
          renderPreview={(file) => (
            <Markdown source={read(open.id, file.path)} />
          )}
          tags={open.tags}
          title={open.name}
        />
      ) : null}
    </>
  );
}
```

## Card, panel, dialog [#card-panel-dialog]

The panel is not a modal and the card is not a button the panel knows about: three
pieces, three jobs, and you can leave any of them out.

* **`SkillCard`** is the summary and nothing else — cover, name, one clamped line. It
  holds no actions of its own, because every action a skill has belongs in the panel,
  where there is room to explain it. Pass `href` instead of `onOpen` and the card
  navigates to a page of its own rather than opening in place. Its cover is drawn at 16:9
  whatever the art is, so a portrait poster loses its top and its tail.
* **`SkillDetail`** is the surface: a rail of identity, examples and description, next to
  a workspace of files and their contents. It fills whatever box you give it — give it a
  height and it behaves like a sheet, leave the height off and it grows with its contents.
  The cover heads the identity column rather than spanning the panel: it is the only colour
  in an otherwise monochrome surface, and keeping it a thumbnail spends 63px of height on it
  instead of 300.
* **`SkillDetailDialog`** is the handful of lines of plumbing: it sizes the panel at
  `min(46rem, 88dvh)` and `max-w-5xl`, names the dialog for assistive tech, and wires the
  panel's close control to closing. Leave it out and the panel sits in a page just as
  happily.

The dialog leans on the panel rather than the other way around: `onClose` is dropped from
`SkillDetailDialog` because the dialog owns it. Everything else passes straight through,
and `open` works controlled or uncontrolled with `defaultOpen`, like the panel's own
selection.

## The head is two-up [#the-head-is-two-up]

The head is the only part of the panel that is read, and it is two columns: the skill on the
left, what it is for on the right. The left column is the identity — cover, name, who made it,
tags — and it holds nothing else, because an action wedged under a byline reads as one more
line of metadata. The name opens that column rather than the tags: a row of chips is metadata,
and metadata does not get the first line of the panel. It also decides where the summary sits —
with the name at the top, the paragraph opposite shares its first line and reads as the name's
expansion, where a paragraph opposite a row of chips reads as their caption. The paragraph opposite is the summary: the one place in the panel where
somebody tells you what the skill does, instead of handing you a file that says it. The action
follows that sentence — right-aligned, 16px under the last line — because proximity is how the
eye decides what a button belongs to. Pushed to the foot of the column instead, it sat under a
paragraph of empty space and belonged to nothing.

Two columns rather than one stack because the two halves answer different questions and
neither of them needs the full width. Stacked, the paragraph sat under the identity, so the
first screen was a cover, a name, a button and a wall of prose before anything else appeared,
and the right half of a 64rem panel stayed empty the whole way down. Now the identity column
is a fixed 24rem, the prose takes what is left and is held to a `max-w-2xl` measure — a
paragraph is not more readable at 1000px — and the examples get the full width underneath
both.

The split happens at 48rem of the panel's own width, with a container query rather than a
viewport one, and below it the two stack identity-first, which is the only order that fits in
a column. The same panel is a dialog here, a sheet there, a route of its own somewhere else;
only its own box knows whether there is room.

The cover is a block, not a thumbnail: it fills the height of the head, so the left edge of
the panel is one flush rectangle instead of a picture floating in a corner with a byline
running past it. That means the art is cropped to the block's height rather than letterboxed
— a cover that keeps its own ratio cannot also line up with a block whose height comes from
the text beside it. Ship two crops of the art: 16:9 for the card, portrait for the panel.

Two more things keep the head short: the toolbar is a header rather than a sticky bar over the
body, and the examples are one ruled strip of three cells rather than three stacked rows or
three cards.

## One gutter, and no grid the panel cannot keep [#one-gutter-and-no-grid-the-panel-cannot-keep]

Every line in the panel starts on the same 24px gutter: the cover, the tags, the heading, the
section label, the example cells, and the first level of the file tree — the tree's pane is
inset 16px and its rows carry the last 8px, so a folder's chevron lands on the same edge as
the heading above it. A panel whose left edge zigzags by four pixels reads as unfinished even
when nobody can say why.

The example strip has no vertical rules. It cannot: three columns and a two-column head have no
common divisor, so a rule under one would never continue the rule above it — and a divider that
lines up with nothing is a grid claim the layout cannot keep. The cells are separated by the
head's own 40px gap instead, which still ties the two bands together without pretending they
share columns.

## Examples, not example cards [#examples-not-example-cards]

An example prompt is a line you might have typed, so it is drawn as one rather than boxed
like a card: a number to scan by, two clamped lines of text, and the whole cell as the
button. Three of them sit across the panel under a single rule, which is the shape that
matches the content — each example is a line, and a line does not need its own bordered panel
to be understood. The arrow is there at rest, dimmed, because a hit target that only admits it
is one under a pointer does not admit it at all on a touch screen.

Side by side also spends the width instead of the height. Stacked, three examples are three
rows of mostly empty space and 120px of the panel's first screen; across, they are 60px.
Below 48rem of panel width the strip stacks back into rows, which is where a column of
lines is the only thing that fits.

The arrow only appears on the cell the pointer is on, and the text lifts to the foreground
with it, so three examples read as things to try rather than three competing panels.
Without `onPrompt` the cells are not buttons at all and render as plain text, because an
example that cannot be sent anywhere is just documentation.

## Files are paths [#files-are-paths]

`files` is a list of paths — `references/api.md` — and folders are implied by the
slashes. There is no nested model to build and nothing is sorted: the order you wrote the
paths in is the order the tree draws them, because a skill folder has an intended reading
order and a generic sort (`SKILL.md` above `LICENSE.txt`) is a guess.

Pass `folder` and that name becomes the root row, expanded, with everything nested under
it collapsed to start — a deep tree opens as a short list. The first file is selected
until told otherwise: a panel that opens on an empty preview pane reads as broken rather
than as unselected.

A folder row carries the disclosure chevron and a folder glyph — open while it is open —
and a file row carries a glyph picked from its extension. Files keep the chevron's width
as an empty slot, so every glyph in a level sits in one column and the labels never
stagger.

Selection and expansion are uncontrolled by default. Pass `selectedPath` and you own the
selection; pass `onFileSelect` and you get told about it either way.

## The preview is yours [#the-preview-is-yours]

The panel hands `renderPreview` the selected `SkillFile` and draws whatever comes back, so
a markdown pipeline, a syntax highlighter, a diff or an iframe all sit behind one prop
without the panel knowing what a file is. Omit it and the tree stands on its own as a
read-only manifest, in a single column.

`SkillCode` is exported for the common case — a labelled, read-only block with a copy
button that confirms in place — and it is deliberately uncoloured. Highlighting a skill's
front matter would mean pulling a highlighter into the bundle to colour four lines that
read fine in the foreground.

```tsx
<div className="flex flex-col gap-6">
  <SkillCode code={frontMatter} language="YAML" />
  <Markdown source={body} />
</div>
```

## Anatomy [#anatomy]

The toolbar is a header, not a sticky bar over the scroller: the body is its own scroll
box, so nothing ever passes underneath the controls and there is no translucent corner to
get wrong. It draws a control only when you pass the handler that makes it do something —
no `onClose` means no close button, and the panel never offers an action it cannot
perform. `menu` is where a "…" dropdown goes when you have one.

The tree is one tab stop with a roving focus, and the arrow keys walk it the way a file
tree is expected to: down and up move a row, right opens a folder (or steps into an open
one), left closes it (or steps back out to the folder that owns the row), `Home` and `End`
jump to the ends. Enter and Space are the row's own click, so a folder toggles and a file
selects.

Because the tree is drawn flat — one list of rows, indented — a collapsing folder animates
every row it owns at once instead of one nested block, and the levels are named in ARIA
(`aria-level`, `aria-posinset`, `aria-setsize`) rather than implied by real nesting. The
enable switch is a real switch with a label of its own (`Enable {title}`), not a
decoration beside the heading.

| Region   | Prop                            | Notes                                                        |
| -------- | ------------------------------- | ------------------------------------------------------------ |
| Card     | `cover`, `title`, `description` | The summary: one line, clamped to two, cover at 16:9         |
| Card     | `onOpen` / `href`               | Opens in place, or navigates when the skill has a page       |
| Identity | `tags`, `title`, `author`       | The heading is the only required prop                        |
| Identity | `cover`                         | Fills the height of the head, cropped; portrait crop         |
| Identity | `updatedAt`                     | Already formatted — the panel does not touch dates           |
| Action   | `onTry` / `tryLabel`            | Closes the summary column, right-aligned; needs a handler    |
| Prose    | `description`                   | The summary opposite the identity, held to a reading measure |
| Prompts  | `prompts` / `promptsLabel`      | Rows become buttons once `onPrompt` is passed                |
| Toolbar  | `enabled` / `defaultEnabled`    | Passing either draws the switch, controlled or not           |
| Toolbar  | `menu`                          | Slot for a "…" dropdown; the panel ships no menu of its own  |
| Files    | `files`, `folder`               | Paths in; the tree is built for you                          |
| Files    | `renderPreview`                 | The only part of the tree the panel does not draw            |
| Dialog   | `open` / `onOpenChange`         | Uncontrolled with `defaultOpen`, like the panel's selection  |

## Motion [#motion]

Expanding a folder is a layout change, so it stays short and eases out, with a fade that
keeps rows from landing at full strength while their height is still near zero. A file's
contents are a place rather than a direction, so the incoming preview fades instead of
sliding in from one side — and only the incoming one animates, so half the duration never
lands between the click and the contents.

Under `prefers-reduced-motion` the rows still open and close, but without the height
animation, and the preview swaps without the fade.

## Component source [#component-source]

<ComponentSource name="skill-detail" src="registry/new-york/agents/skill-detail/index.tsx" title="skill-detail.tsx" />
