Skip to content
Sign inJoin waitlist

Design — Ciketo Next.js

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:3000/style-guide. It shows the colours, text styles, the logo, buttons, the facts, the post card, the accordions, the steps, the ticker, rich text and the form 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; /* text, dark buttons and cards, the footer */
--color-soft: #4b4a4a; /* body text, dates, captions */
--color-grey: #e5e5e5; /* text on dark, lines, form field borders */
--color-orange: #e2652a; /* labels, button hovers, the current page in the menu, step numbers, stars */
--color-background: #f4f0f0; /* the page, form fields, the header once you scroll */
}

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.

Faded colours are opacity modifiers on the same colour, such as bg-ink/70 or text-ink/20: ink/70 is the dark cover over the Home, Contact and 404 photos, ink/50 the cover of the call to action, ink/20 the divider lines and placeholders, white/10 the line in the footer. They follow the palette when you change the colour.

A few components animate a colour with Motion, 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/components/ unless they say otherwise:

Colour Also written in
Black and white actions/Button.tsx (the labels, the small button’s fill), patterns/ServiceAccordion.tsx (open and closed cards)
Orange actions/Button.tsx (the small button’s hover, #e2652a), actions/MenuLink.tsx (the current page, rgba(226, 101, 42, 1))
Grey actions/MenuLink.tsx (the hover fill, rgba(229, 229, 229, …))
Background src/components/site/Header.tsx (the header’s fill, rgba(244, 240, 240, …); white while the phone menu is open)

The form’s error state (“Something went wrong”) uses its own red, in forms/SubmitButton.tsx.

The Ciketo logo (the arrow mark and the wordmark) is drawn as SVG paths in src/design-system/components/brand/Logo.tsx, in the current text colour. That’s why it is black in the header’s white box, white in the footer, and white on phones over the dark covers of Home, Contact and the 404 page. To use your own:

  • As an SVG: replace the path data in mark and wordmark with your logo’s paths and set the viewBox to your artwork’s size.
  • As an image file: replace the <svg> with an <Image> of your file. An image keeps its own colours, also over the dark covers.

It is 94 × 18 px everywhere: change h-[18px] w-[94px] in Logo.tsx to fit your proportions. Its label for screen readers is site.name.

The browser icons are src/app/icon.svg and src/app/apple-icon.png.

The template loads Inter through next/font in src/app/layout.tsx:

const inter = Inter({ variable: "--font-inter", subsets: ["latin"], weight: ["400", "500", "600"] });

theme.css makes --font-inter the site font (--font-sans). To use another Google font, import it by name from next/font/google and keep the variable name:

import { Manrope } from "next/font/google";
const inter = Manrope({ variable: "--font-inter", subsets: ["latin"], weight: ["400", "500", "600"] });

The three weights are regular text (400), headings, buttons and menu links (500), and the form’s messages and the bold starts in rich text (600). For a font you have licensed yourself, use next/font/local and point it at the files in src/app/fonts/. Next.js serves the font 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, button, menu, label

Each is a set of Tailwind classes. The headings have the phone size first and the tablet (md:) and desktop (lg:) sizes after, for example h2: "text-[30px] md:text-[36px] lg:text-[42px] leading-[1.2] font-medium tracking-[-0.03em]". Paragraphs (16 and 14 px), button (16), menu (14) and label (12) are the same at every size. Section labels are the label style in capitals, after a small orange square. Change the numbers here rather than in the components, and headings stay consistent across the site.

Use them as className={text.h2} or combine: className={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 1600 px wide. The side padding is 15 px on phones, 20 px on tablets and 40 px on desktops, and sections are 60 / 80 / 100 px apart (src/design-system/components/layout/Section.tsx).

src/design-system/tokens/motion.ts holds the transitions:

Token Used for
spring Buttons (the orange sweep, the arrow turning), the header’s colour and the phone menu, the services accordion, the steps, the testimonials, the FAQ
springSoft The plus and minus icons, the menu button’s lines, the zoom of a blog card’s photo
springMenu The grey fill behind a menu link on hover
appearText, appearStagger, appearBlock The Home hero on load: the title word by word, then the text, the buttons, the figures and the lane card rising into place

Lower a spring’s duration for a snappier feel, or raise bounce for a livelier one. Apart from the Home hero, nothing animates as it scrolls into view.

Effect Where to change it
The Home hero appears on load <TextAppear> and <Appear> in src/components/sections/HomeHeroSection.tsx; a plain element with the same className shows at once
The client logos move 20 px a second, 60 px apart <Ticker> in src/components/sections/ClientsSection.tsx: add speed={30} or gap={80}
The Blog shows six posts, then six more as you scroll Add pageSize={9} to <BlogListSection> in src/app/blog/page.tsx
The header is transparent at the top of Home, Contact and the 404 page useCover() in src/components/site/Header.tsx and data-header-cover on a page’s first section (Pages and sections)

With “reduce motion” switched on in the visitor’s system, nothing rises, turns or zooms: the Home hero fades in where it stands, the client logos stand still and smooth scrolling is off. Fades and colour changes stay.

Ciketo’s corners are 2 px: buttons, form fields, cards, the photos inside sections and the header’s boxes (rounded-[2px]). The bullets and the squares before labels are square, and there are no drop shadows. Spacing is Tailwind classes on the components (gap-5, p-5, gap-[50px]). Tailwind’s scale is 4 px per step, so p-5 is 20 px. Change them where you see them; the style guide shows the result immediately.

src/design-system/components/ holds the buttons, the menu links, the logo, the clock, the fact rows, the post card, the services accordion, the steps, the FAQ, the ticker, rich text, the form fields and the icons. Its README lists every component next to the design it comes from. The sections that make up a page are one level up, in src/components/sections/, and are listed in Pages and sections.

The icons are Phosphor shapes stored as path data in src/design-system/components/icons/paths.ts (arrow-up-right in the buttons, star in the ratings), and <Icon name="star" /> draws one 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:

    "my-icon": "M128,24A104,104,0,1,0,…",

Then use <Icon name="my-icon" className="size-5" />. Paths use a 256 × 256 grid, which is what Phosphor exports.