Skip to content

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.

<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>

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.

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
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
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

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>
  • Take colours from the roles (text-ink-muted, bg-paper-raised), never from hex values.
  • Use rounded-ui or rounded-media, never rounded-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.

SwiftAgency is made byLessCodexLicence