Section contracts
Shared types every Facade section speaks: heading levels, pluggable image/link components, CTA items.
Installation
pnpm dlx shadcn@latest add https://facadeui.dev/r/types.jsonSource
What the CLI copies into your project, byte for byte.
/**
* Shared contracts for every Facade UI section.
*
* Framework neutrality: nothing here imports from `next/*`. Sections that render
* images or links inside data-driven lists take a *component type* rather than a
* render callback, so `next/image` and `next/link` can be handed over directly
* and still work inside React Server Components:
*
* import Image from "next/image"
* <LogoCloud items={logos} image={Image} />
*
* One-off media uses a `media` ReactNode slot instead — no indirection needed.
*
* a11y: `HeadingLevel` exists so a section never hard-codes `<h2>`. Whoever
* places the section knows the surrounding outline; the section does not.
*
* Dependencies: react (types only).
*/
import type { ElementType, ReactNode } from "react"
/** `<h1>` is reserved for the page; sections start at `<h2>`. */
export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6
/** Maps a heading level to its tag. Use the `Heading` atom to render one. */
export type HeadingTag = `h${HeadingLevel}`
/**
* The subset of image props Facade sections pass through. Intentionally all
* valid DOM attributes so the default `"img"` element works unchanged, while
* remaining a structural match for `next/image`.
*/
export interface FacadeImageProps {
src: string
/** Required. Pass `""` only for images that are genuinely decorative. */
alt: string
width?: number
height?: number
className?: string
sizes?: string
loading?: "eager" | "lazy"
decoding?: "async" | "auto" | "sync"
fetchPriority?: "high" | "low" | "auto"
}
/** Drop-in for `next/image`, or the default `"img"`. */
export type ImageComponent = ElementType<FacadeImageProps>
/** The subset of anchor props Facade sections pass through. */
export interface FacadeLinkProps {
href: string
children?: ReactNode
className?: string
target?: string
rel?: string
"aria-current"?: "page" | "step" | "location" | "date" | "time" | "true" | "false"
"aria-label"?: string
}
/** Drop-in for `next/link`, or the default `"a"`. */
export type LinkComponent = ElementType<FacadeLinkProps>
/**
* Any icon component. Structural rather than nominal, so `lucide-react`,
* `@heroicons/react`, a local SVG component, or anything else that accepts
* SVG props can be passed without the registry depending on that library.
*
* One React Server Components caveat, which bites in exactly one place: an icon
* component cannot be passed as a prop *across* a server-to-client boundary.
* Most icon libraries, `lucide-react` included, do not mark their modules
* `"use client"`, so the reference is a plain function that React refuses to
* serialise. Static sections are server components and render icons in the same
* tree, so they are unaffected. A `-motion` variant *is* a client component, so
* whatever renders it must be a client component too. Adding `"use client"` to
* the file that passes the icons is the whole fix.
*/
export type IconComponent = ElementType<{
className?: string
"aria-hidden"?: boolean | "true" | "false"
strokeWidth?: number | string
focusable?: boolean | "true" | "false"
}>
/** A call to action rendered by `CtaGroup` and by most sections' `actions` slot. */
export interface CtaItem {
label: string
href: string
/** Defaults to `primary` for the first action and `outline` for the rest. */
variant?: "primary" | "secondary" | "outline" | "ghost"
/** Set for links that leave the site; adds `rel="noopener noreferrer"`. */
external?: boolean
"aria-label"?: string
}
/**
* How a section's list is built, so the `-motion` variant can be a real wrapper.
*
* The static section renders `<List>` and `<Item>` from these slots, defaulting
* to plain `ul`/`li`. The motion variant passes Base-UI-free `Stagger` and
* `StaggerItem` adapters instead, which is what lets one file stay entirely free
* of motion imports while the other gets per-item choreography — rather than
* settling for a single fade over the whole band.
*/
export interface ListSlotProps {
/** Wraps the list. Defaults to `"ul"`. */
listAs?: ElementType
/** Wraps each item. Defaults to `"li"`. */
itemAs?: ElementType
}
/**
* The stack equivalent of `ListSlotProps`, for sections whose content is a
* sequence of blocks rather than a list — heroes and CTA bands.
*
* Defaults to plain `div`s, so the static section is a single flex column with
* no wrappers of consequence. The `-motion` variant passes `Stagger` and
* `StaggerItem`, which is how the eyebrow, headline, copy and buttons arrive in
* sequence without the static file importing anything from `motion`.
*/
export interface StackSlotProps {
/** Wraps the content stack. Defaults to `"div"`. */
stackAs?: ElementType
/** Wraps each block in the stack. Defaults to `"div"`. */
blockAs?: ElementType
}
/** Props shared by every section: outline control plus a styling hook. */
export interface SectionBaseProps {
/** Heading level for the section's own title. Defaults to `2`. */
headingLevel?: HeadingLevel
/** Root element. Defaults to `"section"`. Use `"div"` when already inside one. */
as?: "section" | "div" | "article" | "aside"
className?: string
/** Vertical rhythm. Maps to the `--facade-section-y*` tokens. */
spacing?: "sm" | "md" | "lg" | "none"
id?: string
}Props
FacadeImageProps
The subset of image props Facade sections pass through. Intentionally all valid DOM attributes so the default `"img"` element works unchanged, while remaining a structural match for `next/image`.
| Prop | Type | Default |
|---|---|---|
src*Required | string | — |
alt*RequiredRequired. Pass `""` only for images that are genuinely decorative. | string | — |
width | number | — |
height | number | — |
className | string | — |
sizes | string | — |
loading | "eager" | "lazy" | — |
decoding | "async" | "auto" | "sync" | — |
fetchPriority | "high" | "low" | "auto" | — |
FacadeLinkProps
The subset of anchor props Facade sections pass through.
| Prop | Type | Default |
|---|---|---|
href*Required | string | — |
children | ReactNode | — |
className | string | — |
target | string | — |
rel | string | — |
aria-current | "page" | "step" | "location" | "date" | "time" | "true" | "false" | — |
aria-label | string | — |
ListSlotProps
How a section's list is built, so the `-motion` variant can be a real wrapper. The static section renders `<List>` and `<Item>` from these slots, defaulting to plain `ul`/`li`. The motion variant passes Base-UI-free `Stagger` and `StaggerItem` adapters instead, which is what lets one file stay entirely free of motion imports while the other gets per-item choreography — rather than settling for a single fade over the whole band.
| Prop | Type | Default |
|---|---|---|
listAsWraps the list. Defaults to `"ul"`. | ElementType | — |
itemAsWraps each item. Defaults to `"li"`. | ElementType | — |
StackSlotProps
The stack equivalent of `ListSlotProps`, for sections whose content is a sequence of blocks rather than a list — heroes and CTA bands. Defaults to plain `div`s, so the static section is a single flex column with no wrappers of consequence. The `-motion` variant passes `Stagger` and `StaggerItem`, which is how the eyebrow, headline, copy and buttons arrive in sequence without the static file importing anything from `motion`.
| Prop | Type | Default |
|---|---|---|
stackAsWraps the content stack. Defaults to `"div"`. | ElementType | — |
blockAsWraps each block in the stack. Defaults to `"div"`. | ElementType | — |
SectionBaseProps
Props shared by every section: outline control plus a styling hook.
| Prop | Type | Default |
|---|---|---|
headingLevelHeading level for the section's own title. Defaults to `2`. | HeadingLevel | — |
asRoot element. Defaults to `"section"`. Use `"div"` when already inside one. | "section" | "div" | "article" | "aside" | — |
className | string | — |
spacingVertical rhythm. Maps to the `--facade-section-y*` tokens. | "sm" | "md" | "lg" | "none" | — |
id | string | — |