Components
Two kinds of building block: CSS component classes in src/styles/global.css for things that are only styling (buttons, chips, fields), and Astro components in src/components/ for anything with markup or logic. Every one of them reads colour, radius and fonts from the preset, so they look right in all four presets and in dark mode.
Buttons
Section titled “Buttons”<a href="/work" class="btn btn-primary"> View work <Icon name="arrow-right" class="btn-icon" /></a><a href="/contact" class="btn btn-secondary"> Start a project</a><button type="submit" class="btn btn-signal"> Send</button>| Class | Look | Rule |
|---|---|---|
.btn-primary |
Ink fill, turns signal on hover | The main action in a section |
.btn-secondary |
Outline | Everything else |
.btn-signal |
Signal fill | At most one per view |
.btn-icon |
An icon inside a button that nudges right on hover | Use with <Icon> |
Buttons are 48px tall and chips 40px, so both are comfortable to tap.
.link gives an underline that turns to the accent on hover. For big navigational links, add the width-axis effect with .stretch-host and .stretch (see Typography).
Filters and form options. The selected state comes from aria-pressed="true" on a button, or :checked on a hidden input inside a label, so the visual state always matches what assistive technology announces.
<button type="button" class="chip" aria-pressed="true"> Brand</button>
<label class="chip"> <input type="checkbox" name="services" value="Brand" class="sr-only" /> Brand</label>Form fields
Section titled “Form fields”Label above, input with an underline, helper or error below. Connect the error with aria-describedby.
<div class="flex flex-col gap-2"> <label for="name" class="field-label"> Your name </label> <input id="name" name="name" required class="field-input" aria-describedby="name-error" /> <p id="name-error" class="field-error" hidden></p></div>.field-help is the muted helper text. See Contact form for validation.
<Media> wraps Astro’s <Image>: WebP, a responsive srcset, lazy loading and a skeleton shimmer until the image has decoded.
---import Media from '@/components/ui/Media.astro';import cover from '@/assets/studio/studio-1.jpg';---
<Media src={cover} alt="The studio at dusk" ratio="4 / 3" sizes="(min-width: 768px) 50vw, 100vw" />| Prop | Type | Default | Notes |
|---|---|---|---|
src |
ImageMetadata |
An imported image | |
alt |
string |
Required. Use "" for purely decorative images |
|
ratio |
string |
CSS aspect ratio, reserves space before load | |
sizes |
string |
'100vw' |
Tell the browser how wide the image is displayed |
widths |
number[] |
[480, 800, 1200, 1600, 2000] |
Widths larger than the source are skipped |
priority |
boolean |
false |
For the image above the fold: eager, high priority, no fade |
class |
string |
On the wrapper | |
imgClass |
string |
On the <img> |
Set priority on one image per page at most, the one that is likely the largest thing on screen. It makes a measurable difference to LCP.
Phosphor icons, regular weight, inlined as SVG at build time. One family only.
<Icon name="arrow-up-right" size="1.5em" /><Icon name="warning-circle" label="Error" />| Prop | Default | Notes |
|---|---|---|
name |
One of the icons imported in Icon.astro |
|
size |
'1.25em' |
Number (px) or any CSS length |
label |
Makes the icon meaningful to screen readers. Without it, it is hidden | |
class |
On the wrapper |
To add an icon, find it on phosphoricons.com, import its regular SVG at the top of src/components/ui/Icon.astro and add it to the icons map. The name type updates automatically.
Page sections
Section titled “Page sections”Sections are self-contained blocks in src/components/sections/. Each one reads the page language itself, so you only pass it data.
| Component | Props | What it shows |
|---|---|---|
<HomeHero> |
feature: Project |
Headline, intro, both calls to action and the featured project |
<ProjectIndex> |
projects, total |
Ranked list of projects with a preview that follows the cursor |
<Results> |
projects |
Up to four metrics marked highlight in the case studies |
<Capabilities> |
none | What the studio does, as a four-cell bento |
<ClientMarquee> |
clients: string[] |
A slowly scrolling row of client logos |
<Quote> |
quote, name, role, class? |
A large pull quote |
<StudioTeaser> |
none | Short studio intro with a photo, linking to /studio |
<JournalTeaser> |
articles |
The latest articles |
Cards and states
Section titled “Cards and states”| Component | Props | Notes |
|---|---|---|
<ProjectCard> |
project, size: 'wide' | 'narrow', col: 1 | 2, priority? |
Work grid card with its lead metric |
<ArticleCard> |
article, variant?: 'feature' | 'default', priority?, headingLevel? |
feature spans the width with the image beside the text |
<EmptyState> |
title, body |
A hand-drawn sketch that draws itself in, for empty lists |
Brand and chrome
Section titled “Brand and chrome”| Component | Props | Notes |
|---|---|---|
<Wordmark> |
class? |
Your name from site.ts with the slanted signal bar |
<ClientLogo> |
client, class? |
Geometric marks for the demo clients. Replace with real SVG logos |
<ThemeToggle> |
class? |
Light and dark switch; the choice is stored as swift-theme |
<LocaleSwitcher> |
variant?: 'compact' | 'full', class? |
EN / DE / ID, or full language names. Stays on the same page |
<PresetSwitcher> |
none | The demo preset picker. Rendered only when theme.switcher is on |
Long-form text
Section titled “Long-form text”Wrap rendered Markdown or MDX in .prose-swift for article typography: headings on the width axis, accent list markers, pull-quote styling for blockquote, and full-width images.
<div class="prose-swift"> <Content /></div>Writing your own
Section titled “Writing your own”- Take colours from the roles (
text-ink-muted,bg-paper-raised), never from hex values. - Use
rounded-uiorrounded-media, neverrounded-lg, so the preset’s corner rule holds. - Take every visible word from the dictionary with
t(locale). See Languages. - Put interaction in a small module in
src/scripts/and load it from a<script>tag. The whole site ships about 20 KB of JavaScript; keep it that way.