Craft Section

An editorial section block. A small uppercase eyebrow sits above a heading, an optional description softens into the body, and the main content slot fills the rest of the section. When an aside is provided, the body splits into a 2-column grid on wider viewports so the aside rides alongside the content. The component owns the section frame only — eyebrow / heading scale and the vertical rhythm between regions — so it composes with any card, grid, prose, or callout block.

~/craft

Featured

Card 1

A short description for card 1.

Card 2

A short description for card 2.

Customize
Header
Aside

Installation

npx shadcn@latest add https://craftbits.dev/r/craft-section.json

Usage

import { CraftSection } from "@craft-bits/core";
 
<CraftSection
  eyebrow="Featured"
  heading="What we shipped this week"
  description="A pair of editorial entries with a small sidekick lane."
>
  <FeaturedGrid />
</CraftSection>

Add an aside and the body splits into a 2-column grid above md:

<CraftSection
  eyebrow="Chapter 02"
  heading="On the editorial measure"
  aside={<TableOfContents />}
  content={<Prose>{essay}</Prose>}
/>

Drop every slot except content for a pure-body editorial frame:

<CraftSection content={<RecipeCardGrid recipes={recipes} />} />

Understanding the component

  1. Three top regions, one body. The header collects the eyebrow, the heading, and the description; the body holds the content slot and the optional aside. Slots are independently optional — omit any combination and the surrounding rhythm stays intact.
  2. Eyebrow is a small all-caps label. Rendered in var(--cb-font-mono) (with a system-monospace fallback) at 0.6875rem with tracking-[0.15em] so it reads as a kicker, not a heading.
  3. Polymorphic heading level. headingLevel picks h2 (default), h3, or h4 so the section can nest inside another sectioning element without breaking the heading outline. Supply headingId if you need to mirror it on a custom outer container's aria-labelledby.
  4. Aside rides alongside above md. When aside is present the body becomes a grid-cols-1 on narrow viewports and grid-cols-[minmax(0,1fr)_auto] from md upward; asidePosition flips the visual order without changing the DOM order.
  5. gap is a single CSS variable. The chosen gap value (sm / md / lg) lands on --cb-section-gap and powers both the header → body separation and the content ↔ aside gutter in one place.
  6. content wins over children. Either slot fills the body; if both are present content takes precedence so consumers can pass a structured slot without losing the JSX child shorthand for the simple case.
  7. No motion. The component never animates. Theme tokens (--cb-fg, --cb-fg-muted, --cb-bg-elevated, --cb-border-muted) flow in from the surrounding scope.

Variants

Bare editorial frame — eyebrow + heading + content, nothing else:

<CraftSection eyebrow="~/craft" heading="this guy cooks">
  <FeaturedGrid />
</CraftSection>

Centered hero-style section:

<CraftSection
  eyebrow="Showcase"
  heading="What's new"
  description="Three editorial blocks, centered."
  align="center"
  gap="lg"
>
  <ShowcaseGrid />
</CraftSection>

Sidebar-on-the-left layout:

<CraftSection
  eyebrow="Reference"
  heading="API"
  asidePosition="left"
  aside={<TableOfContents />}
  content={<PropsTable />}
/>

Nested section under an h1:

<CraftSection
  eyebrow="Section"
  heading="Recently shipped"
  headingLevel="h3"
  content={<RecipeGrid />}
/>

Props

PropTypeDefaultDescription
eyebrowReactNodeSmall uppercase kicker rendered above the heading.
headingReactNodeMain heading content. Rendered inside the element picked by headingLevel.
descriptionReactNodeOptional supporting copy between heading and body.
contentReactNodeMain body slot. Takes precedence over children when both are present.
childrenReactNodeShorthand body slot. Used when content is omitted.
asideReactNodeOptional aside that rides alongside content above md.
asidePosition'right' | 'left''right'Which side the aside renders on for md and up.
headingLevel'h2' | 'h3' | 'h4''h2'Heading element rendered for heading.
headingIdstringOptional explicit id forwarded to the heading and to the section's aria-labelledby.
gap'sm' | 'md' | 'lg''md'Vertical rhythm between header and body (and the content ↔ aside gutter).
align'start' | 'center''start'Text alignment for the header region.
classNamestringMerged onto the section root via cn().

Accessibility

  • The root renders as a <section> landmark. When heading and headingId are both supplied, the section is labelled by the heading via aria-labelledby so assistive tech announces the region with its title.
  • The eyebrow is plain text; it does not duplicate the heading and stays selectable for keyboard users.
  • Color contrast for the eyebrow / heading / description tones (--cb-fg, --cb-fg-muted) meets WCAG AA against --cb-bg and --cb-bg-elevated in the default light and dark themes.
  • The component ships zero motion, so no prefers-reduced-motion fallback is required.
  • Keep at most one h1 per page — bump headingLevel to h3 or h4 when nesting inside another sectioning region.

Credits

  • Extracted from: terminal-dreams (src/components/retro/CraftSection.tsx). The source was a hardcoded ~/craft block with two inline SVG-illustrated cards linking to /cookbook and /playground, themed via terminal-dreams' --color-accent / --color-surface / --font-mono variables. craft-bits keeps the editorial structure (eyebrow kicker + heading over a body) but reshapes the API: the eyebrow, heading, description, content, and aside become slots; the retro mono treatment falls back to the consumer's --cb-font-mono token; the hardcoded card pair is removed so the section composes with any grid, card, or prose primitive.