A modern, animated landing page built for NORMA S.A. - deep navy-charcoal palette with a confident blue accent, geometric display type, and clean sans-serif body text.
Live site: normasa.gr
This repository showcases the project's design and structure.
A single-page marketing site focused on clarity, trust, and brand consistency - designed around real product photography and a calm, professional tone suited to an industrial/manufacturing client.
- React - component architecture
- Vite - build tooling & dev server
- Tailwind CSS - utility-first styling
- Typography - Space Grotesk (display, headings) + IBM Plex Sans (body) + IBM Plex Mono (mono accents)
| Element | Choice |
|---|---|
| Primary palette | Deep navy-charcoal (ink) + blue accent (accent) |
| Secondary tones | Blue-gray (steel) for muted text/lines, cool off-white (paper) for backgrounds |
| Headings / display | Space Grotesk |
| Body text | IBM Plex Sans |
| Mono accents | IBM Plex Mono |
| Tone | Clean, minimal, industrial-professional |
- Header - sticky navbar with an "N" logo badge and a mobile-responsive menu
- Hero - full-height, centered layout with an accent-colored eyebrow label and a display heading
- Company - "Three generations, one factory": brand story since 1976, with 4 animated stat cards (Founded, Years of experience, Nationwide distribution, International exports) that count up from 0 when scrolled into view
- Products - dark navy section with a responsive product card grid
- Contact - contact details, social icons, and an embedded Google Map
- Footer - minimal, dark navy
A small reusable hook used throughout the site to trigger scroll-based "fade-up" animations and the animated stat counters:
import { useEffect, useRef, useState } from "react";
/**
* useInView
* Returns a ref (attach it to a DOM element) and a boolean that
* becomes true the moment the element scrolls into view.
* Used across the site for "fade-up on scroll" effects.
*/
export default function useInView(options = { threshold: 0.15 }) {
const ref = useRef(null);
const [isInView, setIsInView] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setIsInView(true);
// Once triggered, no need to keep observing
observer.disconnect();
}
}, options);
observer.observe(el);
return () => observer.disconnect();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return [ref, isInView];
}Design tokens (colors & fonts) defined centrally in the Tailwind config:
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,jsx}"],
theme: {
extend: {
colors: {
ink: { DEFAULT: "#06152D", light: "#0B2F66" },
steel: { DEFAULT: "#5C7290", light: "#8FA3BE" },
accent: { DEFAULT: "#1F5FA8", dark: "#003F5C", light: "#5B8FCC" },
paper: "#F0F3F7",
tag: "#F8FAFC",
signal: "#D9A404",
},
fontFamily: {
display: ["'Space Grotesk'", "sans-serif"],
body: ["'IBM Plex Sans'", "sans-serif"],
mono: ["'IBM Plex Mono'", "monospace"],
},
},
},
plugins: [],
};- Respects motion preferences - all animations are disabled automatically for users with
prefers-reduced-motionenabled - Ken Burns effect on the hero background - a slow, continuous zoom + pan that subtly reveals the building photo over time
- Accessible focus states - visible focus outlines on all interactive elements for keyboard navigation
- Custom scrollbar handling for the horizontal product carousel
Picture 1: Hero section
Picture 2: Products section
Design and content © NORMA S.A. This showcase repository is for portfolio purposes only.