> ## Documentation Index
> Fetch the complete documentation index at: https://www.lettercamp.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# UI components

> Keep search, sharing, bylines, contents, and navigation in custom Lettercamp layouts with optional components and documented styling hooks.

Compose your [custom layouts](https://www.lettercamp.com/docs/custom-layout.md) with Lettercamp's optional UI
components. Each provides working UI, default styling, and its existing behavior.
Place them where you want, omit them, or render your own UI from the layout data.
Lettercamp does not automatically insert missing controls into a custom layout.

```jsx theme={null}
import {
  Header, Footer, PostCard, PostHeader,
  PostByline, SearchButton, ShareButton, TableOfContents,
  Pagination, CategoryNavigation, RelatedPosts,
} from 'lettercamp/components';
```

This import works in local previews, standalone builds, and Lettercamp Cloud.
The CLI provides it; you do not install another package. Use the helpers inside
custom React layouts, cards, headers, or footers. Layout overrides hydrate
automatically, so you do not add `client:load` to their nested components.

All public elements accept `className` and `style` on their root element.
`SearchButton` and `ShareButton` also accept normal button attributes and
`children` for a custom label or icon. Keep an accessible label for icon-only buttons.

## Header and Footer

```jsx theme={null}
import { Header as DefaultHeader, Footer as DefaultFooter } from 'lettercamp/components';

<DefaultHeader site={site} pathname={pathname} />
<DefaultFooter site={site} />
```

These are the same elements used by the default site. `Header` includes branding,
navigation, the primary link, and the mobile menu. `Footer` includes branding,
link columns, social links, and the Lettercamp credit.

Pass `site` to use the current `header`, `footer`, and branding settings.
`Header` uses `pathname` to mark the current navigation link. Both accept
`className` and `style` on their root. Publication-relative links from settings
are resolved automatically.

Importing these elements always renders the default; it never calls your
`components/layouts/Header.jsx` or `Footer.jsx` override. That lets an override
safely wrap the default element.

## PostCard

```jsx theme={null}
import { PostCard as DefaultPostCard } from 'lettercamp/components';

<DefaultPostCard post={post} site={site} priority={false} />
```

Renders the standard cover, date, category, title, description, and authors.
Pass a normalized `post` from your layout. `priority` defaults to `false`; set it
for the first listing card to load its image eagerly. `className` and `style`
apply to the article root.

This import always renders the standard card. In contrast, the `PostCard` **prop**
provided to `PostList` and `PostLayout` selects your card override when one exists,
and otherwise uses this default. `RelatedPosts` makes the same selection.

## PostHeader

```jsx theme={null}
<PostHeader post={post} site={site} />
```

Renders the same article header as the default site: category, title, description,
author/date/reading-time byline, sharing controls, and cover image. It does not
include the article body, TOC, or related posts; your layout arranges those.

| Prop | Default and behavior |
| - | - |
| `post` | Required; normalized article metadata from `PostLayout` |
| `site` | Built settings; pass the layout's live `site` for preview updates |
| `layout` | Follows `site.post.heroLayout`, defaulting to `"stacked"`; use `"split"` for a horizontal hero |
| `showCover` | Follows `site.post.showCover`; explicit boolean overrides it |
| `showShare` | `true`; controls both responsive sharing buttons |
| `className`, `style` | Applied to the header root |

Reading-time visibility follows `site.post.showReadingTime`. The header uses
`site.post.contentWidthEm` and the post's cover sizing metadata. To replace the
byline or title structure, compose your own header with `PostByline` and
`ShareButton` instead.

## PostByline

```jsx theme={null}
<PostByline post={post} site={site} />
<PostByline post={post} site={site} showDate={false} showReadingTime={false} />
```

Displays publication date, author names and avatars, the additional-authors
popup, and reading time. Pass the normalized `post` supplied to your custom
layout or card.

| Prop | Default and behavior |
| - | - |
| `post` | Required; uses `authors`, normalized `date`, and `readingMinutes` |
| `site` | Built site settings; pass the layout's live `site` for online settings previews |
| `showAuthors` | `true`; hides names, portraits, and the popup when false |
| `showAvatars` | `true`; controls portraits in the byline |
| `showDate` | `true`; no date appears when the post has no valid date |
| `showReadingTime` | Follows `site.post.showReadingTime`; an explicit boolean overrides it |

For cards, use `showReadingTime={false}` to match the default card behavior.
You can also display `post.authors`, `post.date.label`, or `post.readingMinutes`
with your own markup.

## SearchButton

```jsx theme={null}
<SearchButton />
<SearchButton className={styles.search}>Find an article</SearchButton>
```

Opens the shared search dialog with matching, highlighted results, loading and
empty states, arrow-key navigation, Escape dismissal, and focus restoration.
Several buttons on a page share one dialog. The default label is “Search articles…”
with a search icon; narrow screens show the icon. Custom children replace that
content. Set `aria-label` when you need a different accessible label.
You can place a button in an article layout or header as well as a post list.

Search covers published article titles, descriptions, and extracted body text.
It uses the generated `search-index.json` beneath your publication's base URL.
The index updates when you build or publish. Online draft previews still search
that last built index; unsaved drafts are not added to search results.
Search results keep their own UI rather than using your custom `PostCard`.

For completely different search behavior, build your own UI. The index is a JSON
array with `title`, `href`, `summary`, `text`, `category`, and formatted `date`;
`image` and `imageAttributes` may also be present. Use supplied URLs directly.
The default search does not require a custom search service or a hook API.

## ShareButton

```jsx theme={null}
<ShareButton />
<ShareButton className={styles.share}>Share this article</ShareButton>
```

Opens the existing share dialog, including copy link, sharing destinations,
and summarization links. The default label is “Share” with an icon.
The dialog reads the current page's canonical URL and metadata when opened,
including updated metadata in a draft preview. **It shares the current page**;
putting it in a listing card does not make it share that card's destination.

## TableOfContents

```jsx theme={null}
<TableOfContents headings={headings} site={site} />
```

Defaults to the floating control on the left edge, matching the vertical
article layout. Hover, focus, or tap the indicators to open the section links.
It tracks the active heading; Escape closes it. Without JavaScript, the heading
links render in the document flow. No dropdown wrapper or positioning CSS is
needed.

| Prop | Default and behavior |
| - | - |
| `headings` | Array of `{ depth, slug, text }` from `PostLayout`; empty arrays render nothing |
| `site` | Pass live settings for preview updates |
| `title` | `"Contents"`; heading above the links |
| `variant` | `"floating"` by default; `"sidebar"` renders a sticky sidebar for your layout to position |
| `show` | Follows `site.post.showTableOfContents`; an explicit boolean overrides it |

The starter explicitly selects `sidebar` for a horizontal (`split`) article.
The sidebar is hidden at narrow widths in the standard article layout; the
floating variant remains available. Heading targets stay inside Lettercamp's
managed article body, so keep rendering `children`.

## Pagination

```jsx theme={null}
<Pagination {...pagination} />
```

Renders previous/next links, page numbers, ellipses, disabled end controls, and the
current-page state. It renders nothing when there is only one page.

Pass `current`, `total`, and `base`, all included in `PostList`'s `pagination` prop.
`base` is the supplied listing URL, including its category and any publication
subfolder. Do not prepend the subfolder again. Pagination follows normal links;
Lettercamp handles those links inside the online preview too.

## CategoryNavigation

```jsx theme={null}
<CategoryNavigation categories={categories} category={category} />
```

Renders “All” and the supplied category links, marking the selected category.
`categories` contains `{ name, href }` objects; `category` is the selected name
or `""` for all posts. Both come from `PostList`. Set `allLabel` to change “All.”
Use CSS to arrange the links as tabs, pills, or a wrapping list.

## RelatedPosts

```jsx theme={null}
<RelatedPosts posts={relatedPosts} site={site} pathname={pathname} title="Keep reading" />
```

Renders the supplied related posts using your `PostCard` override, or Lettercamp's
default card when you have no override. Lettercamp calculates the related posts;
the component presents them.

| Prop | Default and behavior |
| - | - |
| `posts` | Use `PostLayout`'s `relatedPosts`; empty arrays render nothing |
| `site` | Pass live settings for the cards and preview updates |
| `pathname` | Pass the layout's current path through to custom cards |
| `title` | `"Related"` |
| `show` | Follows `site.post.showRelatedPosts`; an explicit boolean overrides it |

The default grid uses three columns, two at tablet widths, and one on small screens.
Override the grid with CSS, or map `relatedPosts` yourself for a different structure.

## Style the components

Use CSS Modules for your layout and the documented global classes for component
parts. For example, pass `className={styles.byline}` and style it locally:

```scss theme={null}
/* PostLayout.module.scss */
.byline:global(.lettercamp-post-byline) {
  gap: 20px;
  font-size: 14px;
  :global(.author-portrait) { width: 32px; height: 32px; }
  :global(.author-avatar) { border-radius: 8px; }
}
```

Import a global stylesheet once from your layout for dialogs, which render
outside your layout wrapper:

```scss theme={null}
/* theme.scss, imported with: import './theme.scss'; */
html .post-search-dialog {
  width: min(720px, calc(100vw - 32px));
  border-radius: 20px;
  background: #181818;
  color: white;
}
html .post-search-result { padding: 16px; }
html .post-search-match { background: #ffe08a; color: black; }
```

These classes are supported styling hooks:

| Component | Root and parts |
| - | - |
| Header | `.lettercamp-header`, `.site-header-inner`, `.brand`, `.header-actions`, `.desktop-navigation`, `.header-primary`, `.mobile-navigation` |
| Footer | `.lettercamp-footer`, `.footer-main`, `.footer-columns`, `.footer-column`, `.footer-bottom`, `.footer-socials` |
| Post header | `.lettercamp-post-header`, `.article-intro`, `.article-category`, `.description`, `.byline`, `.article-hero-image` |
| Post card | `.lettercamp-post-card`, `.post-card-cover`, `.post-card-content`, `.post-card-details`, `.post-card-link`, `.post-card-description`, `.post-card-meta` |
| Byline | `.lettercamp-post-byline`, `.post-authors`, `.author-portraits`, `.author-portrait`, `.author-avatar`, `.author-names`, `.author-more`, `.reading-time`, and `time` |
| Authors popup | `.authors-popover`, `.authors-popover-list`, `.authors-popover-author` |
| Search | `.lettercamp-search-button`, `.post-search-dialog`, `.post-search-header input`, `.post-search-close`, `.post-search-status`, `.post-search-results`, `.post-search-result`, `.post-search-image`, `.post-search-title`, `.post-search-meta`, `.post-search-excerpt`, `.post-search-match`, `.post-search-empty` |
| Share | `.lettercamp-share-button`, `.post-share-dialog`, `.post-share-heading`, `.post-share-preview`, `.post-share-actions`, `.post-share-action`, `.post-share-menu`, `.post-share-menu-item`, `.post-share-status` |
| Contents | `.lettercamp-toc`, `.article-toc`, `.toc-trigger`, `.toc-subheading`, `.toc-label`, and `a[aria-current="location"]` |
| Pagination | `.lettercamp-pagination`, `.pagination-direction`, `.pagination-pages`, `.pagination-gap`, and `a[aria-current="page"]` |
| Categories | `.lettercamp-categories` and `a[aria-current="page"]` |
| Related posts | `.lettercamp-related-posts`, its heading, and its direct `.post-list` child |

Use `::backdrop` to style the search and share dialog backdrops. Shared colors and
fonts use the site's CSS variables, including `--site-background`,
`--site-foreground`, `--site-border`, and `--font-family`.
Keep visible focus states and readable contrast, and avoid changing interaction
attributes or hiding controls the reader needs. CSS changes appearance; it does
not replace search ranking, popup behavior, or sharing actions.

## Preview and editing

The starter's `examples/layouts/` files use these elements and standard styles.
Copy them to `components/layouts/` to customize the arrangement.
They run inside the online custom-layout preview using the last built components
and current draft data. Pass the layout's `site` prop to settings-aware helpers;
omitting it uses the built settings instead of live settings changes.

Article text, tables, images, and other rich blocks remain managed through
`children`. These UI helpers do not change the editor's content format.
Custom headers and footers still use the existing online placeholders. Helpers
placed inside an MDX article follow the usual custom-MDX placeholder behavior.

Publish component or stylesheet changes to update their online preview bundle.
Use `lettercamp dev` while developing locally.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.