← Back to case study

Component Reference

Portfolio Design System — Dev Handoff Documentation

Every reusable element on this site follows the same pattern: clearly typed props, consistent use of design tokens, and predictable interaction states. A new case study page can be added by writing content and passing props to existing components, with no new CSS required.

ProjectCard

ProjectCard Reusable

Displays a project with thumbnail, metadata, description, and CTA. Renders as a list item within the projects grid.

Visual Preview

Default ProjectCard in default state showing ComBoard project with teal CTA
Hover ProjectCard in hover state with lifted shadow and cursor on CTA
Focus-visible ProjectCard with yellow focus outline around the CTA button
Disabled ProjectCard in disabled state showing Lidia Paints with Coming Soon badge

Props

PropTypeRequiredDescription
imagestringYesThumbnail image path
altstringYesImage alt text
datestringYesTimeline range (e.g. "4/24 – 9/25")
rolestringYesDesigner role on project
titlestringYesProject name (rendered as h3)
descriptionstringYesOne-line project summary
hrefstringNoLink to case study page
disabledbooleanNoShows "Coming Soon" badge instead of CTA
accentColorstringNoSets --card-accent for theme color

Usage

<!-- Active project with custom accent --> <ProjectCard image="/images/comboard/thumbnail.png" alt="ComBoard app showing ETRAN keyboard" date="4/24 – 9/25" role="UXR & Design" title="ComBoard" description="ETRAN keyboard reduced eye strain" href="/comboard" accentColor="#4cc4cf" /> <!-- Upcoming project (disabled) --> <ProjectCard image="/images/placeholder.png" alt="Lidia Paints preview" date="Coming 2026" role="Art" title="Lidia Paints" description="Personal art portfolio" disabled />

Tokens Used

PropertyTokenValue
Card radius--radius-lg20px
Body padding--space-md24px
Image aspect ratio4 / 3
Border--border-neo-thick2px sides, 4px right + bottom
Meta font0.8125rem / 600
Title font--font-headingBricolage Grotesque 700
Body font--font-body0.9375rem / 500
CTA radius--radius-fullpill (9999px)

States

Guidelines

Do
  • Use inside a <ul> grid for proper list semantics
  • Provide a unique accentColor per project for visual distinction
  • Write concise descriptions under two lines
  • Always include meaningful alt text for the thumbnail
Don't
  • Use for non-project content (blog posts, team bios, etc.)
  • Omit href on active projects — every shipped project needs a link
  • Set disabled and href together — pick one
  • Reuse the same accentColor for adjacent cards

Responsive Behavior

BreakpointLayoutNotes
≥ 768px3-column gridCards share equal width, images maintain 4:3
< 768pxSingle columnCards stack full-width, same internal spacing

SideNav

SideNav Reusable

Sticky sidebar navigation with scroll-spy highlighting. Displays section links with an animated highlight pill that follows the active section.

Visual Preview

Default (active on first section) SideNav with The Problem highlighted as active section

Props

PropTypeRequiredDescription
sections{ id: string; label: string }[]YesArray of section IDs and display labels

Usage

<SideNav sections={[ { id: "understanding-users", label: "Understanding Users" }, { id: "journey-map", label: "Journey Map" }, { id: "defining-challenge", label: "Defining the Challenge" }, { id: "design-process", label: "Design Process" }, { id: "outcomes", label: "Outcomes" }, ]} />

Tokens Used

PropertyTokenValue
Sticky offsettop: 80px
Link padding4px 12px
Link font0.8125rem / 500
Highlight radius--radius-fullpill (9999px)
Highlight color--color-projectPer-project theme color
Animation--transition-base300ms ease
Gap6px

States

Guidelines

Do
  • Match id values to actual section IDs on the page
  • Keep labels short (2–3 words) so they fit without wrapping
  • Set --color-project on the page to match the project theme
Don't
  • Use on pages with fewer than 3 sections — not worth the chrome
  • Duplicate the SideNav — only one per page for scroll-spy to work
  • Add external links — this is for in-page navigation only

Responsive Behavior

BreakpointBehavior
≥ 992pxVisible as sticky sidebar, scroll-spy active
< 992pxHidden entirely (display: none). Users scroll freely or use the header nav.

PrevNextNav

PrevNextNav Reusable

Project pagination bar shown at the bottom of case study pages. Displays previous and next project links.

Visual Preview

Hover (previous link) PrevNextNav showing Budget App as previous and A11y Consulting as next, with hover on previous

Props

PropTypeRequiredDescription
prev{ title: string; href: string }NoPrevious project link and title
next{ title: string; href: string }NoNext project link and title

Usage

<!-- Both directions --> <PrevNextNav prev={{ title: "ComBoard", href: "/comboard" }} next={{ title: "Portfolio Site", href: "/portfolio-site" }} /> <!-- First project (no previous) --> <PrevNextNav next={{ title: "Portfolio Site", href: "/portfolio-site" }} />

Tokens Used

PropertyTokenValue
Container gap--space-md24px
Padding--space-sm16px
Border radius--radius-md12px
Label font0.75rem / 600, uppercase
Title font1rem / 600
Top border--color-border1px solid

States

Guidelines

Do
  • Place at the very bottom of each case study, after all content
  • Omit prev on the first project and next on the last
  • Use the exact project title as it appears in the card
Don't
  • Link to external pages — this is for internal project navigation only
  • Use on the homepage or non-case-study pages
  • Pass both props as empty — if there is no navigation, omit the component

Responsive Behavior

BreakpointBehavior
≥ 0pxAlways visible. Flexbox layout with space-between. Both links remain side-by-side at all widths. Text truncates naturally if the viewport is very narrow.
Header Layout

Sticky site header with logo, desktop pill navigation, projects dropdown, and mobile hamburger menu. No props — navigation items are hard-coded.

Visual Preview

Desktop Header with logo and color-coded pill navigation buttons: Projects, View 1-Pagers, About Me, Email Lidia, Resume
Projects dropdown open Header with Projects dropdown expanded showing color-coded links to all case studies
Mobile (closed) Header on mobile showing logo and hamburger menu icon
Mobile (open) Mobile menu expanded showing all navigation links as color-coded pill buttons

Structure

ElementDescription
LogoLinks to home, 48px height
Nav pillsColor-coded pill buttons with individual inline accent colors
Projects dropdownClick-triggered, closes on outside click or Escape
Mobile menuHamburger toggle, full nav list, visible below 960px

Usage

<!-- No props. Include once in the base layout. --> <Header /> <!-- Typical layout structure: --> <body> <SkipLink /> <Header /> <main id="main-content"> ... </main> <Footer /> </body>

Tokens Used

PropertyTokenValue
Background--color-nav-bg#faf9f6 / #222
Positionsticky, top: 0, z-index: 1000
Inner max-width--max-width1200px
Pill padding--space-xs / --space-md8px 24px
Pill radius--radius-fullpill (9999px)
Pill font0.875rem / 500
Border--border-neo-thick2px + 4px offset

States

Guidelines

Do
  • Include exactly once per page, at the top of the layout
  • Pair with <SkipLink /> for keyboard accessibility
  • Update the dropdown project list when new case studies are added
Don't
  • Override pill colors via external CSS — edit the inline styles in the component
  • Remove the mobile menu — it is the primary nav under 960px
  • Nest inside a container with overflow: hidden — the sticky position and dropdown will break

Responsive Behavior

BreakpointBehavior
≥ 960pxFull desktop nav with pill buttons and Projects dropdown. Mobile toggle hidden.
< 960pxDesktop pills hidden. Hamburger toggle appears. Tapping it reveals a full-screen mobile nav with all links stacked vertically.
Footer Layout

Site-wide footer with CTA heading, social links, and contact form. No props — content is hard-coded.

Visual Preview

Desktop (2-column) Footer with CTA heading, Let's Connect and LinkedIn buttons, contact form with Name, Email, Message fields, and copyright
Mobile (single-column) Footer on mobile with CTA section stacked above contact form

Structure

ElementDescription
CTA sectionHeading, body text, Email + LinkedIn pill buttons
Contact formName, Email, Message fields via Web3Forms API
Footer bottomCopyright + attribution

Usage

<!-- No props. Include once at the bottom of the layout. --> <Footer /> <!-- The contact form uses Web3Forms. --> <!-- The API key is set inside the component. --> <!-- Success state is handled via inline JS. -->

Tokens Used

PropertyTokenValue
Background--color-footer-bg#cbedf0 / #1e3a4a
Text--color-footer-text#2d2d2d / #e8e8e8
Grid layout1col → 2col at 768px
Padding--space-xl / --space-lg64px top, 40px bottom
Top border--color-shadow3px solid
Link pills--radius-fullSame neo-brutalist style as header

States

Guidelines

Do
  • Include once per page, always as the last element before </body>
  • Test the contact form after deploying — Web3Forms needs a valid API key
  • Keep the CTA heading action-oriented and personal
Don't
  • Add navigation links here — that's the header's job
  • Remove the aria-live region — it announces form success to screen readers
  • Expose the Web3Forms API key in client-side logs

Responsive Behavior

BreakpointBehavior
≥ 768pxTwo-column grid: CTA + links on left, contact form on right
< 768pxSingle column: CTA section stacks above the contact form

Design Tokens

All components draw from a shared set of CSS custom properties defined on :root. Dark mode overrides are handled via prefers-color-scheme: dark.

Spacing

--space-xs0.5rem (8px)
--space-sm1rem (16px)
--space-md1.5rem (24px)
--space-lg2.5rem (40px)
--space-xl4rem (64px)
--space-2xl6rem (96px)

Radii

--radius-sm8px
--radius-md12px
--radius-lg20px
--radius-full9999px (pill)

Colors

--color-accent#ffd44f
--color-orange#ffad00
--color-teal#4cc4cf
--color-green#75d349
--color-mauve#bea7be
--color-pink#f13989

Typography

--font-headingBricolage Grotesque
--font-bodyCommissioner
Base size1rem (16px)
Line height1.6

Borders

--border-neo2px solid
--border-neo-thick4px (right + bottom)
--color-shadow#2d2d2d / #555

Transitions

--transition-fast150ms ease
--transition-base300ms ease