Custom components
Add your own components and override kit modules, parts and the layout from site.config.ts. Covers design tokens, which overrides exist, and how to keep an override working across upgrades.
On this page
The kit draws every module of a generated site: the hero, the feature grid, the FAQ and the rest. You change how they look in three steps, from least to most work: change a design token, fill a layout slot, or override a component. This page covers all three, and your own components.
Kit files are locked, so you never edit them. You register a replacement instead. The replacement is a file of yours, in the site zone, that no tool rewrites. See Ownership zones.
Choose the smallest change
| You want to | Use |
|---|---|
| Other colours, fonts, radius or spacing | A design token |
| Something extra in the header, footer or banner, or in the head | A layout slot |
| A different look for one module, such as the hero | A module override |
| A different card grid, image, navigation, search or SEO | A part override |
| A component that no module offers, used by your own pages | A site component |
An override is yours to maintain. Every kit upgrade can change the props or the markup of the component you replaced, so prefer a token or a slot when one is enough.
Design tokens
The look of a site comes from the @theme block in src/styles/global.css. The kit’s components use these tokens and never fixed values. design.md is the source: apply-design.mjs writes its tokens into the block when the site is scaffolded, and the block says that the tokens mirror design.md one to one. To change the design, edit design.md, run the regenerate skill (it runs apply-design.mjs, and the contrast must pass), and then adjust the components that follow from it. A design.md that is still a draft is not applied. Applying the design replaces the tokens that design.md lists and adds the ones that are missing. A token that you added to the block by hand and that design.md does not list is left as it is. A token that design.md does list is overwritten with the value from design.md.
| Group | Tokens |
|---|---|
| Colour | --color-canvas, --color-surface, --color-line, --color-ink, --color-muted, --color-accent, --color-accent-strong, --color-on-accent |
| Fonts | --font-serif, --font-sans |
| Widths | --container-site (72rem), --container-column (42rem), --container-prose (72ch) |
| Shape | --radius-card, --radius-button |
| Spacing | --spacing-section, --spacing-block, --spacing-page. The kit also uses --spacing-tight and --spacing-gutter. |
| Image caps | --spacing-image-body (20rem), --spacing-image-square (16rem), --spacing-image-logo (5rem), --spacing-image-hero (28rem) |
| Buttons | --color-action, --color-action-ink, --color-action-hover, --font-action |
Tailwind turns them into classes such as bg-canvas, text-ink, rounded-card, mt-section and gap-block.
Button tokens
Buttons use the action tokens, and links keep --color-accent. When the action tokens are not set, they fall back to the accent values. That lets a site have near-black buttons and rust-coloured links without any override. Do not override the hero or the CTA only to change a button colour or font. If you already did, npx mdlake migrate --buttons can replace the override with tokens. See Upgrade and migrate.
Contrast
scaffold-site checks the contrast of the colour pairs of design.md against WCAG AA (4.5:1). If you change a colour, check the pair yourself, and run npm run check, which includes the accessibility check at desktop and phone widths.
The layout and its slots
Every page is drawn by the kit Layout (@mdlake/site-kit/layouts/Layout.astro). It has four named slots. Fill one from a page of yours, or from a layout that wraps it.
| Slot | Default content |
|---|---|
head | Nothing extra. Use it for a <link> or a <meta> tag. |
banner | The preview banner when a candidate lake or a CMS preview is on, otherwise nothing. |
header | The logo and the navigation. |
footer | The footer columns. |
| default | The page content. |
---
// src/pages/offer.astro
import Layout from '@mdlake/site-kit/layouts/Layout.astro'
import { getLakeDocument } from '@mdlake/site-kit/content'
const offer = await getLakeDocument('offer')
---
<Layout title={offer.title} description={offer.summary}>
<link slot="head" rel="preload" href="/fonts/display.woff2" as="font" type="font/woff2" crossorigin />
<div class="mdl-column" set:html={await offer.html()} />
</Layout>Important
A banner slot that you fill replaces the preview banner. Keep the banner when the page can be previewed, or editors will not see that the page shows a preview.
To change the layout of the whole site, register your own layout with layout in site.config.ts. It may wrap the kit layout and fill its slots. Change the header, footer and home page shows this from start to end.
site.config.ts
site.config.ts is optional and site-owned. Create it when you need your first override.
// site.config.ts
import { defineSiteConfig } from '@mdlake/site-kit'
export default defineSiteConfig({
modules: { hero: './src/components/MyHero.astro' },
layout: './src/layouts/SiteLayout.astro',
parts: { LakeImage: './src/components/MyLakeImage.astro' },
})| Key | What it replaces | Names it accepts |
|---|---|---|
modules | A page module | hero, featureGrid, steps, testimonial, stats, faq, cta, richText, caseHeader, docsPage, form, endOfPage |
layout | The page layout | A path |
parts | A shared part | CardGrid, CodeCopy, DocsSearch, DocsSidebar, FormFields, LakeImage, MobileNav, Prose, SEO |
Rules:
- Each value is a path from the site root, such as
./src/components/MyHero.astro. - Everything you do not name comes from the kit.
- Restart
astro devafter you change the file. The dev server reads it at start. - Write a module or part override as an
.astrofile, like the kit’s own. The kit places a module on the page without aclient:directive, so a.tsxfile registered as a module would not become interactive. For an interactive piece, write a.tsxisland and use it inside your.astrooverride (or a site component) with aclient:directive such asclient:load.
Override a kit module
- Look at what the module receives. The component gets the same props as the kit’s. Every module gets
data(what the module resolved from the lake and the reading rules).featureGrid,steps,statsandfaqalso getindex, the position of the module on the page.richTextandcaseHeaderalso getback, the link to the page that lists the collection (href,labeland an optionalheading), or nothing. Read the kit’s component for the exact shape, for examplenode_modules/@mdlake/site-kit/dist/components/modules/Hero.astro, but do not edit it there. Find what to change lists the file of each module. - Write your component in
src/components/. Keep the props of the kit component. - Register it in
site.config.ts. - Restart
astro devand runnpm run check.
---
// src/components/MyHero.astro
import LakeImage from '@mdlake/site-kit/components/LakeImage.astro'
import type { Resolved } from '@mdlake/site-kit/lib/modules.js'
interface Props { data: Extract<Resolved, { module: 'hero' }> }
const { data } = Astro.props
const { image } = data
---
<section class="mdl-wide grid items-center gap-block pt-page md:grid-cols-2 md:pt-section">
<div>
<h1 class="font-serif text-ink">{data.headline}</h1>
{data.subline && <p class="mt-6 text-muted">{data.subline}</p>}
{data.actions.map((action) => (
<a href={action.href} class="rounded-button border border-action bg-action px-5 py-3 font-action text-action-ink no-underline">
{action.label}
</a>
))}
</div>
{image && <LakeImage asset={image} alt={image.alt} aspect="4 / 3" loading="eager" class="rounded-card" />}
</section>The Resolved type comes from @mdlake/site-kit/lib/modules.js, which the package exports through ./lib/*. The kit’s own components import it by relative path.
The hero’s data has headline, subline, actions (each with label, href and a style) and image. Other modules have their own shapes. Read the kit’s component for the module you override, because the shapes can change between kit versions.
Content still comes from the lake. Your override receives the resolved text as props, so the plan and the reading rules keep working. Do not import lake text into the component.
If npm run check fails with an astro check error in an override after an upgrade, the kit changed the props. Compare your file with the kit’s new one and port the change.
Override a kit part
Parts are the pieces that several modules share. The card grid, the image, the prose renderer and the SEO tags are examples. A part override replaces it everywhere. Register it under parts.
Warning
A LakeImage override opts out of the kit’s image pipeline, which gives responsive AVIF and WebP, srcset and lazy loading. Copy the <Picture> branch of the kit’s component if you still want those.
Leave the kit’s imports alone
The kit’s own components import each other through the aliases @mdlake/site/module/*, @mdlake/site/part/* and @mdlake/site/layout. These aliases resolve to your override when you registered one, and to the kit’s file when you did not. That is how a part override reaches the modules that use it. In your own code, import from the package paths shown on this page (@mdlake/site-kit/...). The aliases are made for the kit’s own files. Nothing in the kit or the template documents them for site code, so do not depend on them there.
Add a site component
For something that is not a module, such as a pricing table that three of your pages use, write a component of your own. It lives in src/components/, is site-owned, and is never touched by refresh-content, regenerate or upgrade-ruleset.
Rules for a component:
- Style it with Tailwind classes on the tokens and with the tokens of
global.css. Write no hard-coded colours, font sizes or pixel spacing. - Interactive elements use
react-aria-components. Write an island as.tsx. - Icons come from Lucide only:
@lucide/astroin Astro files,lucide-reactin React islands. Do not draw SVGs or use Unicode glyphs as icons. - Content arrives as props. The page reads it with
getLakeDocumentorgetLakeCollectionand passes it down. The component never imports lake text. See Custom pages. - No server code. A server route is API routes and a form is Forms.
- Accessibility: semantic HTML first, WCAG 2.1 AA, a visible focus state.
npm run checkruns the axe check.
---
// src/components/PlanCard.astro
import { ArrowRight } from '@lucide/astro'
interface Props {
title: string
summary: string
href?: string | undefined
}
const { title, summary, href } = Astro.props
---
<div class="rounded-card border border-line bg-surface p-block">
<h3 class="font-serif text-ink">{title}</h3>
<p class="text-muted">{summary}</p>
{href && (
<a href={href} class="text-accent">
Read more <ArrowRight aria-hidden="true" />
</a>
)}
</div>Use it from a page:
---
import PlanCard from '../components/PlanCard.astro'
import { getLakeCollection } from '@mdlake/site-kit/content'
const plans = await getLakeCollection('plans')
---
{plans.map((plan) => <PlanCard title={plan.title} summary={plan.summary} href={plan.route} />)}Check and commit
npm run checkThe check builds the site, tests the bindings and runs the accessibility check. Look at the result in npm run dev and compare it with design.md. Commit only your own files. The commit-site.mjs script the skill uses takes a list of paths and refuses nothing that is yours.
What can go wrong
| Symptom | Cause and fix |
|---|---|
| The override is ignored | The path in site.config.ts is wrong, or the dev server was not restarted. |
astro check fails in an override after an upgrade | The kit’s props changed. Port your component to the new props. |
| Edit hook blocks a file | It is a kit file or a locked file. Register an override instead. |
| A button has the wrong colour in one module only | A leftover override. Replace it with tokens (migrate --buttons). |
| Contrast fails in the accessibility check | A colour pair in design.md is below 4.5:1. Change the tokens through design.md. |
| Icons look wrong or missing | You used a glyph or an icon from another set. Use Lucide. |
What’s next
- Change the header, footer and home page for a worked example of a layout and a module override.
- Working with AI to have Claude Code write an override with the
add-componentskill. - Site configuration for the full list of keys.
