Design — Tustel Astro
The look comes from a small set of tokens. Change a token and nearly every page, component and state follows, so you rarely need to touch a component.
See everything first
Section titled “See everything first”Run npm run dev and open http://localhost:4321/style-guide. It shows the colours, the text styles, the section
title, the buttons and links, the logo, the arrow icon, the menu button, the project and post cards, the specialities
card, the logo ticker, the counters, the form with its button’s states and rich text, with their names. When you want
to know what a change will affect, look there.

Colours
Section titled “Colours”src/design-system/theme.css:
@theme { --color-ink: #000000; /* headings, text, dark buttons, the covers over photos */ --color-soft: #434343; /* body text, labels, captions, dates */ --color-orange: #eb4b3d; /* hovers, the chosen service, the active project, required-field asterisks */ --color-background: #f5f4f2; /* the page and the solid header */}Each name becomes a Tailwind class: bg-ink, text-soft, bg-orange. Swap the hex values for your own palette and
the site changes. Keep enough contrast between text and its background (at least 4.5:1), or the site becomes hard to
read. White is Tailwind’s own white: the footer, the form fields, the specialities card and the white buttons over
photos.
Faded colours are opacity modifiers on the same colour, such as bg-ink/60 or bg-white/30: ink/60 is the dark cover
over the Home and 404 photos (and the slideshow arrows), ink/30 the labels under a project’s title, the line in the
About hero and the form’s placeholders, ink/10 the lines between a project’s details, white/30 the line in the Home
hero. The photo band darkens towards ink at the bottom. They follow the palette when you change the colour.
A few parts animate a colour with Motion, which animates values, not class names, so they write the value out instead of
using the token. Change these too when you change a colour; search the file for the value. The paths start in
src/design-system/ unless they say otherwise:
| Colour | Also written in |
|---|---|
Black (#000000) and white (#ffffff) |
components/actions/button.ts (the boxes, labels, squares and arrows of the four button styles), components/actions/TextLink.astro (the social links) and components/actions/MenuLink.astro (the menu links); black also in src/components/sections/ServicesSection.astro with scripts/services.ts, for the services that are not chosen, and in src/components/sections/PortfolioSection.astro with scripts/portfolio.ts, for the projects that are not active in the List view |
Orange (#eb4b3d) |
components/actions/button.ts and components/actions/TextLink.astro (the hovers), src/components/sections/ServicesSection.astro and scripts/services.ts (the chosen service), src/components/sections/PortfolioSection.astro and scripts/portfolio.ts (the active project in the List view) |
| Background | src/components/site/Bar.astro and src/components/site/header.ts (the solid header’s fill, rgba(245, 244, 242, …)) |
The form keeps a few colours of its own: the field borders in components/forms/field.ts, the select’s grey
placeholder in components/forms/FieldControl.astro, the pressed button and the red error state (“Something went
wrong”) in components/forms/SubmitButton.astro. The style guide’s colour swatches repeat the hex values as text, in
src/pages/style-guide.astro.
The Alex Carter logo (the AC monogram in a ring, the name and a small ® mark) is drawn as SVG paths in
src/design-system/components/brand/Logo.astro, in the current text colour. That’s why it is white over the Home and
404 photos, and black on the solid header and in the footer. To use your own:
- As an SVG: replace the path data in
fills(the filled shapes) andstrokes(the outlines, each with its stroke width) with your logo’s paths, and set theviewBoxto your artwork’s size. - As an image file: put it in
public/and replace the<svg>with an<img>of it. An image keeps its own colours, also over the photos of Home and the 404 page, so pick one that reads on both.
It is 104 × 24 px everywhere: change h-6 w-[104px] in Logo.astro to fit your proportions. Its label for screen
readers is site.owner. The same artwork is in src/assets/images/logo.svg (black) and
src/assets/images/logo-white.svg (white) for use elsewhere; the site itself draws the logo from Logo.astro.
The browser icons are public/icon.svg (the monogram; it turns white in a dark browser theme) and
public/apple-icon.png.
The template loads Inter with the Astro Fonts API, in astro.config.mjs:
fonts: [ { provider: fontProviders.google(), name: "Inter", cssVariable: "--font-inter", weights: ["100 900"], styles: ["normal", "italic"], subsets: ["latin"], fallbacks: ["sans-serif"], options: { experimental: { variableAxis: { opsz: [["14", "32"]] } } }, },],Inter is one variable font here: every weight, the italic (for the emphasis in posts) and its optical-size axis
(variableAxis, from 14 to 32). The text styles pin that axis, which gives two looks from one file: 32 for the
headings (Framer’s “Inter Display”) and 14 for everything else, written as [font-variation-settings:'opsz'_32] in
src/design-system/tokens/text.ts. The <Font> line in src/layouts/Layout.astro loads it on every page, and
theme.css makes --font-inter the site font (--font-sans).
To use another Google font, change name and keep cssVariable:
name: "Manrope",cssVariable: "--font-inter",weights: ["200 800"],styles: ["normal"],Leave out options unless the new font has an optical-size axis: Google Fonts refuses the request otherwise, and the
build warns No data found for font family … and leaves the site on the fallback font. Without the axis, the
font-variation-settings in the text styles simply do nothing; without an italic, the browser slants the regular face
for the emphasis in posts. weights is Inter’s whole range: give a variable font its own (["200 800"] for Manrope),
or list the three weights the site uses for a font that isn’t variable, ["400", "500", "600"]: regular text,
headings, buttons, labels and menu links, and the form’s “Thank you” and “Something went wrong”. For a font you have
licensed yourself, use fontProviders.local() with your files, for example in src/assets/fonts/ (see the
Astro fonts guide). Astro downloads the font when the site builds and
serves it from your own domain either way, so there is no request to Google at runtime.
Text styles
Section titled “Text styles”src/design-system/tokens/text.ts holds the sizes, weights and line heights, named after the design:
display, h1, h2, h3, h4, h5, h6paragraph, small, menu, button, label, tinyEach is a set of Tailwind classes. The headings are Inter Display at weight 500, with the phone size first and the
tablet (md:) and desktop (lg:) sizes after: display is 54 / 62 / 92 px, h1 36 / 44 / 54, h2 32 / 36 / 42, down
to h6 18 / 19 / 20. Paragraph (16 px), small (14), menu (14), button (15), label (15) and tiny (12) are Inter at the
same size everywhere. Every style has −0.03em letter spacing. Section labels are the label style in capitals. Change
the numbers here rather than in the components, and text stays consistent across the site.
Three sizes live with their component: the counters’ numbers (40 / 46 / 54 px) in
src/design-system/components/data/FactList.astro, the phone menu’s links (24 px) in src/components/site/Bar.astro,
and the form fields’ 16 px text in src/design-system/components/forms/field.ts.
Use the styles as class={text.h2} or combine: class={cn(text.h6, "text-white")}.
Breakpoints
Section titled “Breakpoints”Three sizes, set in theme.css:
| Width | Tailwind prefix | |
|---|---|---|
| Phone | below 810 px | (none) |
| Tablet | 810–1199 px | md: |
| Desktop | 1200 px and up | lg: |
Content is at most 1440 px wide. The side padding is 15 px on phones, 20 px on tablets and 30 px on desktops, and most
sections have 60 / 80 / 100 px above and below (sectionGutters and sectionPadding in
src/design-system/components/layout/section.ts).
Motion
Section titled “Motion”src/design-system/tokens/motion.ts holds the transitions. The scripts in src/design-system/scripts/ (and the
header’s, src/components/site/header.ts) play them with Motion:
| Token | Used for |
|---|---|
spring |
The social and menu links (the orange underline and fill, the colour), the header’s fill and fade, the phone menu opening |
springQuick |
The Catalog / List switch on Home and the highlighted project in the List view |
springSoft |
The menu button’s lines turning into a cross, and the services on Home (the chosen one’s colour, the accordion on phones) |
slideSpring |
The slideshows: the Home hero photos and the testimonials |
fadeUp, fadeUpDistance, loadUpDistance, loadUpDelay |
Blocks fading up 18 px as they scroll into view, or 7 px on page load (<Reveal>) |
heroBlockEase |
The Home hero’s buttons, card and social links rising 50 px on load (<HeroBlock>) |
appearEase, letterStagger, wordStagger, lineStagger |
The text effects (<TextAppear>): letters, words or lines appearing 0.05 s apart |
Lower a spring’s duration for a snappier feel, or raise bounce for a livelier one.
Hovers are attributes in the markup, played by src/design-system/scripts/hover.ts: data-hover marks the element
you point at, data-motion gives each part that changes its two states (rest and hover), and data-transition names
the token it moves with (spring, quick or soft). Keyboard focus shows the hover state too. The buttons (the orange
band, the rolling label, the turning square and the sliding arrow) and the project and post card hovers have no
data-transition, so they move with Motion’s own defaults: a quick spring for moves and turns, 0.3 s for sizes and
colours. Add data-transition="spring" to a part in Button.astro, ProjectCard.astro or PostCard.astro to give
it the links’ slower spring.
| Effect | Where to change it |
|---|---|
| The Home hero shows a new photo every 5 s | interval={5} on <Slideshow> in src/components/sections/HomeHeroSection.astro |
| The testimonials move on every 8 s | interval={8} in src/components/sections/TestimonialsSection.astro |
| The client logos move 20 px a second, 50 px apart | speed={20} and gap={50} on <Ticker> in src/components/sections/ClientsSection.astro |
| The specialities in the Home hero card scroll up 5 px a second | speed={5} (twice) in src/design-system/components/patterns/SpecialtiesCard.astro |
| Texts appear on load: the Home hero letter by letter, page titles word by word, labels and introductions line by line | <TextAppear> in the sections (by="letters", "words" or "lines"); a plain element with the same class shows at once |
| Blocks fade up as they scroll into view | <Reveal> in the sections; a plain element with the same class shows at once |
| The counters count up by one every 5 ms | src/design-system/scripts/counter.ts |
| Portfolio and Blog show six items on desktop and four below, then more as you scroll | pageSize={6} and smallPageSize={4} on <PagedList> in src/components/sections/ProjectListSection.astro and PostListSection.astro |
| The header over the Home photo, and the header that hides as you scroll down | src/components/site/Header.astro and header.ts (Pages and sections) |
With “reduce motion” switched on in the visitor’s system, movement is switched off: texts and blocks fade in where they
stand instead of rising, button labels and arrows no longer roll and turn, the slideshows no longer move on by
themselves, the tickers and loading spinners stand still and the counters show their final number at once. Fades,
colour changes and the hover fills stay. play() in src/design-system/utils/play.ts, which every script animates
through, does most of this; the slideshow and counter scripts check the setting themselves, and the tickers and spinners
stop in CSS (motion-reduce:animate-none).
Corners, spacing, shadows
Section titled “Corners, spacing, shadows”Tustel’s corners are square: buttons, form fields, cards and photos. Only the portraits (the testimonials and the About
hero), the slideshow arrows and the spinners are round, and there are no drop shadows (the form fields draw their 1 px
border with an inset shadow). Spacing is Tailwind classes on the components (gap-5, p-3, gap-[50px]). Tailwind’s
scale is 4 px per step, so gap-5 is 20 px. Change them where you see them; the style guide shows the result
immediately.
Components
Section titled “Components”src/design-system/components/ holds the buttons and links, the logo, the arrow icon, the text effects and scroll
reveals, the menu button, the section layout, the project and post cards, the images, the slideshow, the counters, the
tickers, the specialities card, the paged list, rich text and the form fields. Their behaviour is in
src/design-system/scripts/. The README lists every component next to the Framer
component it comes from, and every script with what it does. The sections that make up a page are in
src/components/sections/, and are listed in Pages and sections.
The arrow in the buttons is a Phosphor shape stored as path data in
src/design-system/components/icons/paths.ts (arrow-right), and <Icon name="arrow-right" /> draws it in the
current text colour. To add one:
-
Open the icon on phosphoricons.com and copy the SVG.
-
Take the
d="…"of its<path>. -
Add a line to
iconPaths:"my-icon": "M128,24A104,104,0,1,0,…",
Then use <Icon name="my-icon" class="size-5" />. Paths use a 256 × 256 grid, which is what Phosphor exports. The
slideshow’s two arrows are chevrons drawn in src/design-system/components/media/Slideshow.astro.