Overview
Sections are pure content components. They own their content — headings, grids, forms, actions — but they do not own surface concerns (background, vertical padding, border-radius) or container concerns (content width, gutter). Surface and layout are the caller’s responsibility via composition:
- LayoutSection — full-width band: background + vertical padding + Container (content width + gutter)
- SurfaceBox — constrained surface: background + caller-applied rounding and padding via Tailwind utilities
class— ad-hoc spacing or styling overrides on the component’s root element
This three-layer composition model keeps each concern in exactly one place and gives callers full control over how sections sit within page layouts.
Step 1: Create the Component File
Place in src/core/components/sections/ (// CORE-OWNED). Use PascalCase naming: MySection.astro. Add // CORE-OWNED at the top of the file.
Step 2: Define the Props Interface
Import only the shared types you need from @/lib/section-types. Do not extend SectionProps — that interface is for LayoutSection, not for section content components.
import type {
SectionAlign,
SectionHeadingLevel,
SectionHeadingSize,
} from "@/lib/section-types";
interface Props {
heading: string;
// component-specific props
align?: SectionAlign;
headingLevel?: SectionHeadingLevel;
headingSize?: SectionHeadingSize;
class?: string;
[key: string]: unknown;
}
Key rules:
- No
background,verticalPadding,contentWidth,gutter, orrounded— these are surface/container concerns owned by the caller - No
asprop — section components render<div>, not<section>. The caller’sLayoutSectionalready renders the<section>element, so using<div>avoids nested sections - Always include
[key: string]: unknown— enables attribute passthrough (id,aria-*,data-*)
Step 3: Destructure with Defaults
const {
heading,
align = "left",
headingLevel = 2,
headingSize,
class: className,
...rest
} = Astro.props as Props;
The ...rest collects arbitrary HTML/Astro attributes for forwarding to the root element.
Step 4: Structure the Root Element
The root element is a <div> with BEM class, alignment modifier, and rest-spread for attribute passthrough:
<div
class:list={[
"my-section",
align !== "left" && `my-section--align-${align}`,
className,
]}
{...rest}
>
<Heading level={headingLevel} size={headingSize} class="my-section__heading">
{heading}
</Heading>
<!-- section content -->
</div>
No surface classes (surface--bg-*), no padding variant classes (--vp-*). The component renders content only.
Step 5: Use Project Primitives
Headingfor headings (not raw<h>tags)Buttonfor actionsIconfor icons- Consume semantic tokens (
--st-*), never raw color values
Step 6: Add BEM Class Hooks
Root element gets my-section. Children get my-section__element. These are CSS API hooks for // SITE-OWNED style overrides.
Step 7: Styling Approach
- Tailwind utilities first.
- Scoped
<style>only for compound/complex selectors, variant variable binding, and alignment variants. - No
@apply. - No surface CSS — no
surface--bg-*classes, nopadding-blockvariant blocks.
Step 8: Caller Composition
Callers wrap the section with LayoutSection or SurfaceBox to provide surface and container framing:
---
import LayoutSection from "@core/components/sections/LayoutSection.astro";
import MySection from "@core/components/sections/MySection.astro";
---
<!-- Full-width band with background and padding -->
<LayoutSection background="soft" verticalPadding="2xl">
<MySection heading="Features" items={features} />
</LayoutSection>
<!-- Inside a constrained surface (e.g., a card) -->
<SurfaceBox background="contrast" class="rounded-xl p-lg">
<MySection heading="Highlights" items={highlights} />
</SurfaceBox>
Existing Section Types for Reference
All section components follow the pure-content pattern. Good starting points:
FeatureGrid.astro— card grid with structured data arrayCallToAction.astro— the only section that retains anasprop (for cases where the caller doesn’t use LayoutSection)