Nitip Design System (Astro)
Reusable tokens and components for every page of the Nitip template. The published Framer template is the reference for look and behavior; component names follow the Figma file so designers and developers talk about the same thing.
See it live at /style-guide (not linked from the site, not indexed).
Structure
Section titled “Structure”src/ design-system/ ← this folder: page-agnostic building blocks theme.css ← color, breakpoint, easing and animation tokens (Tailwind v4 @theme) tokens/text.ts ← text styles (h01 … menu) tokens/motion.ts ← springs and the height reveal utils/ ← cn(), link helpers, motionStates(), image lookup scripts/hover.ts ← hover states for data-hover / data-motion (loaded by the layout) components/ ← actions, brand, navigation, layout, cards, media, data, patterns, forms, cursor, utils types.ts ← data shapes the components take (ProjectCardData, …) components/ site/ ← Header, Footer (design system + site content) sections/ ← page sections, content passed in as props content/ ← site.json, pages/*.json, collections/*.json, schemas.ts, routes.ts lib/ ← content.ts (collections), to-ui.ts (content → props), format.ts, forms/- Design-system components never import from
src/content. Data comes in through props, so the same component works on any page. - Pages pass content to sections; sections compose design-system components. Keep copy out of components.
- Links come from
routes(routes.project(slug)) or the content, never hard-coded strings. - Use tokens, not raw values:
text.h02,bg-ink,border-line,springButton. If a new value repeats, add a token. - Match Framer first. Any deliberate refinement is documented in the component’s comment.
- No framework runtime. Interactivity is plain TypeScript in each component’s
<script>(Astro bundles it once per page) using Motion’sanimate,hoverandinView. - New component? Put it in the right
components/group, add it to/style-guideand the table below.
Hover states
Section titled “Hover states”A hoverable element gets data-hover; every element that changes on hover — the element itself included — gets
data-motion with its two states (write the rest state inline too, so the page is right before scripts run):
<a href={href} data-hover data-motion={motionStates({ boxShadow: rest }, { boxShadow: hover })} style={`box-shadow: ${rest}`}> {label} <span data-motion={motionStates({ rotate: 0 }, { rotate: 45 })}><Icon name="arrow-up-right" /></span></a>data-transition="soft" picks another spring (default button). Hover is ignored on touch; with reduced motion the change
is instant.
Tokens
Section titled “Tokens”| Token | Values |
|---|---|
| Breakpoints | Phone < 810 · md Tablet 810–1199 · lg Desktop ≥ 1200 |
| Colors | ink #101010 · canvas #F6F6F6 · line #D9D9D9 · muted #818181 · accent #FC5000 · star #FDCA49 · danger #FF2244 |
| Font | Inter variable (font-sans), loaded by the Astro Fonts API in astro.config.mjs |
| Text styles | text.h01 46/58/72 · h02 44/52/64 · h2 32/36/42 · h3 28/32/36 · h4 24/26/28 · h5 20/22/24 · h6 18/19/20 · hugeParagraph 24/28/32 · largeParagraph 18 · paragraph 16 · smallParagraph 14 · button 14 uppercase · menu 13 uppercase |
| Section spacing | <Section>: sectionGutters 20/30/40, sectionPadding 60/80/100 · sectionGap 60/80/100 between header and content · pageTitlePadding 50 top for page titles (layout/section.ts) |
| Motion | springButton (spring 0.4s, bounce 0.2) · springSoft (0.4s, no bounce) · springSlide (200/40) · springCursor (500/60) · reveal (height reveal) · ease-framer CSS tween for image zooms |
Components
Section titled “Components”| Component | Figma name | Notes |
|---|---|---|
PrimaryButton |
Primary Button (Default / No Icon) | icon={false} for No Icon. Hover: shadow + arrow 45° |
SecondaryButton |
Secondary Button | Hover: underline + arrow -45° |
BigButton |
Big Button | Footer menu link |
SocialMediaLink |
Social Media | Hover: black row, white text |
MenuButton |
Menu (Default / Hover / Active) | Active has no hover |
TextLink |
— (Framer link preset) | 80% ink + underline on hover; external links open in a new tab |
ArrowButton |
— (Framer slideshow arrow) | |
SwitchButton |
Switch Button (Default / Active) | Square toggle, #222 outline; Default hover 50% opacity |
LoadMoreButton |
Load More | Square ink button, 150 × 40, no hover |
Logo |
Logo (Black / White) | tone="light" for dark backgrounds |
Icon |
Phosphor icon set | name, tone, size via class |
Navigation |
Navigation (Default / Home / …) | Active item from pathname; layout="stack" for mobile |
Hamburger |
Hamburger (Default / Close) | The header script opens the menu |
Breadcrumbs |
— (Framer “Breadcrumbs”) | Last item is the current page |
Section, SectionHeader, SectionLabel, Divider |
Section / Title Section | SectionHeader size="small" for Heading 6 titles; leadClass sets the lead width |
PageTitle |
Page Title | Breadcrumbs (optional), h1, divider, description (max 600, descriptionWidth="wide" 850) |
ProjectCard |
Project With Details (Default) | “View Project” cursor on the image; titleAs="h2" on index pages |
ProjectSimpleCard |
Project Simple Card (Open / Close) | Used by ProjectRecentList |
ProjectListItem |
Project With Details (Desktop / Tablet / Phone List) | Row with divider; View Details and image zoom on desktop |
PostCard |
Post Card | “Read” cursor |
TestimonialCard |
Testimonial (Desktop / Tablet / Mobile) | |
TextPoint |
Text Point | List row with dot |
HighlightList |
— (Framer rich text) | Bulleted list with a bold lead phrase |
FactCard |
Fact Number | tone="dark" for the ink card; ring cursor |
TeamCard |
Team | Portrait, name, role, email. No hover |
FillImage |
— | Optimized image filling its positioned parent (src = /images/<file>) |
CoverImage |
— (Framer Image + Cover) | Bottom gradient with content; ring cursor |
ParallaxImage |
— (Framer Parallax) | speed/phoneSpeed/offset/scale; image sized so no gap ever shows; clip={false} to clip in the parent |
ScrollTransform |
— (Framer Scroll Transform, In View) | from/phoneFrom (x, y, scale, opacity) → rest while the frame scrolls into view |
Chart |
Chart (Desktop / Desktop Start / Mobile) | Grows on scroll; ring cursor |
PricingCard |
Pricing | Plan switch (hidden with one plan), included list, price, CTA |
Rating, AvatarGroup |
Hero “User Rating” | |
AccordionItem |
Service / Careers row | layout="service" | "career"; marker in the marker slot, panel in the default slot |
ServiceList |
Service (Open / Close) | |
CareerList |
Careers (Open / Close) | Dot marker; pageSize adds a left-aligned Load More |
ValueList |
Value (Desktop / Tablet / Phone) | Numbered rows. No hover |
ProjectRecentList |
Project Recent List | Hover opens a card |
ProjectGallery |
Projects page Grid / List | Switch + ProjectCard grid or ProjectListItem list |
PostGrid |
Blog archive grid | 1 / 2 / 3 columns, Load More, 4 or 6 posts per page |
RichText |
— (Framer rich text) | Post body: headings 40 above, paragraphs and lists 20; numbered and nested lists, bold leads |
Slideshow, TestimonialSlideshow |
Testimonial Slide Show | Autoplay 5s, drag, keyboard |
Ticker |
— (Framer Ticker) | 30 px/s, CSS only |
BigFooter |
Big Footer | “More Details” cursor |
Form |
— (Framer form) | Demo, static endpoint or server route (src/lib/forms/README.md); pending / success / error on the Submit button; resets on success only; honeypot |
CheckboxGroup |
— (Framer checkboxes) | Every checked option is sent under one name |
FormField |
— (Framer input / textarea) | Label + 40 px input or 200 px message box |
SubmitButton |
Form (Default / Loading / Success / Error) | Full width; status for a fixed state |
Cursor |
Cursor (Dot / Active / View Project / Read / More Details) | Rendered once in the layout; use data-cursor="…" |
CurrentYear |
— | Always-current copyright year |
Cursor
Section titled “Cursor”<Cursor /> is rendered once in src/layouts/Layout.astro. Mark elements with data-cursor:
<a href={href} data-cursor="view-project">…</a> <!-- also: "read", "more-details", "ring" -->