Skip to content
Sign inJoin waitlist

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.

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.

The style guide page: the colours and text styles, then every component with its name

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) and strokes (the outlines, each with its stroke width) with your logo’s paths, and set the viewBox to 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.

src/design-system/tokens/text.ts holds the sizes, weights and line heights, named after the design:

display, h1, h2, h3, h4, h5, h6
paragraph, small, menu, button, label, tiny

Each 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")}.

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

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

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.

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:

  1. Open the icon on phosphoricons.com and copy the SVG.

  2. Take the d="…" of its <path>.

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