kerryclements.com
Case study
Overview
A personal portfolio site designed to demonstrate both front end craft and UX thinking. Built with React and TypeScript, designed in Figma with a token-based design system, and deployed on Netlify.
The brief
Between roles and actively looking for front end and UX positions, I needed a portfolio that reflected my positioning: a front end developer who brings UX thinking to their work. The site needed to demonstrate both skills, not just list them.
Design decisions
I designed the site in Figma before writing any code. A token-based colour system was established first, with tokens named by role rather than value: bg/primary, text/secondary, accent/rose, to mirror how CSS custom properties would be implemented and to support future theming.
A WCAG AA contrast failure was caught during the design phase. The text/tertiary colour was changed from #666666 to #888888 after a Stark plugin check revealed a 3:1 ratio against the background. The final ratio is 5.1:1. This was caught before a single line of CSS was written.
The layout is desktop first at 1440px with a 1200px content area. The primary audience is recruiters reviewing portfolios on desktop. Full-width case study cards give an editorial feel and will scale naturally as more projects are added.
Development decisions
Key decisions made during the build:
- The portfolio route was moved outside the Layout component. The portfolio has its own nav and doesn't need the existing site header
- CSS tokens in
portfolio.cssmatch Figma colour style names exactly, keeping design and code in sync - Magic strings extracted to a
constants.tsfile: email address, LinkedIn URL, and CV path are defined once and referenced throughout - Work cards and skills are driven by data arrays, so adding a new project or skill requires no changes to JSX
- Nav and Footer extracted as shared components. Adding GitHub to the footer once updated it across every page, now there is no hunting for duplicate markup
- Kerry Clements in the Nav and Footer links to the homepage, keeping that detail consistent without extra effort
- Contact was removed from the homepage Nav. The link anchors to the footer, which works on longer pages, but on the homepage (currently just a hero) the page is too short to scroll, making the link appear broken. It will be restored once the homepage has enough content
- The data-driven work cards paid off immediately: Jobs Done was added to both the portfolio and apps pages with no JSX changes
- Legacy standalone nav and footer implementations removed once the shared components were verified in place
Mobile responsive pass
The site was built desktop-first at 1440px and needed a mobile audit before being usable on phones. The main issues were fixed horizontal padding (120px on nav, hero, cards, and footer), a three-column skills grid, and work cards with a fixed-width side-by-side image and content layout, all of which broke down well before 390px.
A single breakpoint at 768px addresses these: padding drops to 20px, the skills grid collapses to one column, and work cards switch from a row to a column layout with the image stacked above the content.
The CV download button surfaced a separate cross-browser issue. Linking directly to the PDF with a download attribute opens Chrome's built-in PDF viewer instead of downloading the file, while the same markup works as expected in Edge. Since recruiters are more likely to want the file saved for later (to forward or open in their own PDF reader) the fix fetches the PDF as a blob and triggers the download programmatically, which bypasses the browser's native viewer entirely and behaves consistently across browsers.
Accessibility
Alt text was added via the Stark plugin throughout the design, since Figma has no native alt text field. A full accessibility check was run on the complete page design with 0 violations before the build began.
After launch, a manual keyboard test revealed the photo lightbox failed WCAG 2.1.1 (Keyboard). The trigger was a clickable image, unreachable by Tab. The original implementation was a custom overlay driven by useState. The fix replaced it with a semantic <button> trigger and a native <dialog>, which provides Escape-to-close, focus containment, and focus return without a library, and removed more custom code than it added. The :focus-visible styles match the accent token, the close target was enlarged to 44px, and a new bg/overlay token was added to both Figma and CSS for the backdrop.
Testing on mobile also surfaced a touch-specific issue: :hover styles on the nav photo and nav links were sticking after a tap, since touch interactions can trigger :hover on devices with no pointer to "unhover" with. Both rules were wrapped in @media (hover: hover) so they only apply on devices with a genuine hover capability. The photo button's :focus-visible outline was split out from this rule first, so keyboard focus styling remains unaffected on all devices.
A further Stark audit caught unlabelled region violations across four pages. Any <section> element implicitly carries a landmark role, and a landmark without a name gives screen reader users no way to distinguish or navigate between them. An aria-label was added to every <section> on the homepage, portfolio, apps, and contact pages. 98% score, 1 violation on the homepage becoming 100% across all four pages after the fix.
Contact page
The homepage's Contact link previously anchored to the footer, which only worked once the page had enough content to scroll. On a hero-only homepage this appeared to do nothing. The fix was a dedicated /contact route, giving room for a proper "Connect" section (email, LinkedIn, GitHub, CV download, each with a one-line caption) and a separate "Social Media Work" section highlighting content management experience for a local synagogue's Instagram account.
Key decisions: no contact form, since email is already public site-wide and a form adds complexity (spam handling, Netlify Forms) without real benefit. Personal Instagram and X were excluded as they didn't serve the portfolio's professional framing. The footer's existing links were kept rather than removed, since it acts as a global utility while the Connect section adds page-specific context.
Building the page also resolved the lingering touch hover issue flagged earlier: an unscoped .nav__links a:hover rule, duplicating the one already scoped to @media (hover: hover), was still applying on touch devices. Removing it fixed the stuck-hover state site-wide. Several case study pages also had hardcoded #contact anchors in their nav links, updated to /contact individually since each page's nav configuration differs slightly.
Discoverability
While looking at other developers' portfolios with an AI tool, I noticed it could read their sites but not mine. All it saw were my meta tags. The site is a client-side React app, so the server sends an empty <div id="root"> and the content only appears once JavaScript runs. Browsers were fine, but anything that doesn't run JavaScript saw a blank page. That includes the AI tools a recruiter might use to summarise a candidate, link previews, and some search crawlers.
As a quick fix I enabled Netlify's Prerender extension, which serves fully rendered HTML to crawlers and AI agents while visitors still get the normal app. I checked it by requesting the page with curl and a Googlebot user agent.
Reading the rendered HTML for the first time surfaced issues I hadn't spotted in the browser:
- Two cards had alt text copied from the Berakhot card, so screen reader users heard a description of the wrong app
- Every page declared the homepage as its canonical URL, which tells search engines those pages are duplicates of the homepage
- The Skills column headings were
<h3>s with no<h2>above them, so they nested under the last project card in the heading outline - Every page shared the same title and description
The alt text and heading fixes were quick. For the rest, I added a root layout route with a SiteMeta component. Each route carries its own title and description in its React Router handle, and SiteMeta renders the <title>, meta description, canonical, and Open Graph tags. React 19 hoists these into <head> on its own, so no extra library was needed. It also replaced three hand-written useEffect hooks on the personal pages that had been setting the title and canonical directly on the DOM.
What's next
- Additional case studies as projects are completed
- Scrolling video walkthrough embedded in this case study
- Move from the Prerender extension to build-time prerendering with React Router framework mode, so every visitor gets real HTML, not just crawlers
- Consistent colour modes across the site. The main pages are dark and the personal pages are light, so clicking through to Personal is a jarring jump to a bright page. Both will follow the visitor's system setting with
prefers-color-scheme, adding a light mode to the main pages and a dark mode to the personal pages, while the personal pages keep their own palette