This is a solution to the space tourism website challenge on Frontend Mentor, which provides image assets and a style guide in a Figma file. There are no guidelines in the challenge for which technologies to use.
Live demo: https://space-tourism-brochure.vercel.app/
Users should be able to:
- View the optimal layout for each of the website's pages, depending on their device's screen size
- See hover states for all interactive elements on the page
- View each page and be able to toggle between the tabs to see new information
Additionally:
- Use semantic HTML and styling to aid context for accessibility
- Implement client side routing to allow responsive navigation between pages (prevents the hang time of requests to server for next page)
- React - JS library
- Vite - Front End Developer Tooling
- React Router v6 - Client side page routing in React apps
- Excalidraw - Diagram drawing
-
Examine design file, start planning HTML element architecture and CSS classes to create reusable styles.
-
Before adding any styles, start scaffolding content in JSX components.
-
Write out the base styles for your project, including general content styles, such as
font-familyandfont-size. -
Style static components.
-
Plan component and routing architecture using Excalidraw.
-
Write JSX components for base page layout and navbar.
-
Style homepage and navbar, using CSS grid for element layout.
-
Write components, layout, style for Destination page.
-
Set up React Router for interpage navigation.
-
Intrapage tab navigation (with keyboard nav implemented).
-
Using Destination page as a basic guide, fill out Crew and Technology pages.
-
Easy way to create a button or link that:
- is circular - has text centered within it
.button-or-anchor {
display: grid;
place-items: center;
padding: 0 2rem;
border-radius: 50%;
aspect-ratio: 1;
}- Simple addition to make keyboard navigation of the website easier, using an anchor tag that jumps to the main content of the site. The anchor tag is placed as the first element in the body, which makes it the first item focused when
TABis pressed.
<a className="jump-to-content" href="#main">
Skip to content
</a>.jump-to-content {
position: absolute;
z-index: 9999;
background: hsl(var(--clr-white));
color: hsl(var(--clr-dark));
padding: 0.5em 1em;
transform: translateY(-100%);
transition: transform 100ms ease-in;
}
.jump-to-content:focus {
transform: translateY(0);
}- CSS organization
One primitive strategy for organizing styling in an app is simply keeping everything in one big file. This is simple, but it can work. I decided against it as I started to build out the site because was annoying to scroll through (which is a hint that something can be improved) and cognitively fatiguing to debug.
If you are using Tailwind CSS, in the tailwind.config you can specify background images per url path.
I decided to create CSS files per each page's component, but still keep commonly used components (and utility classes, and base styles) together in index.css
- Using the picture tag to use newer and better image formats (like .webp), while allowing the browser to fallback to older formats if the newer formats are unsupported (like .jpg or .png).
<picture srcSet="/src/assets/crew/image-douglas-hurley.webp" type="image/webp">
<img src="/src/assets/crew/image-douglas-hurley.png" alt="Douglas Hurley" />
</picture>- Strategies for loading dynamic page content
i. Individual files for each page
+ Extremely simple to implement, minimal JavaScript needed.
- Hard to maintain and extend - lots of duplicate markup. If new content on any page is added or changed, it is much more work to update the website than just adding/modifying an entry to the CMS (Content Management System) or data.json file.
ii. Write all content in elements for each page, then hide and unhide the relevant elements using JS as needed.
+ Simple to implement.
- Bad for performance.
- Still hard to maintain and extend for the same reasons as in the previous approach.
iii. Query the CMS (or in the case of this simple web app, the data.json) for the relevant content.
+ Most scalable and maintainable approach.
- Extra complexity to set up and hook into a CMS.
After laying out and styling the content on each page, I decided to use queries to the data.json to dynamically load in content. For a simple static site like this, any of the approaches could be appropriate, I just wanted to practice something a little harder but more extendable.
- Patterns for changing feature image source based on media queries
In the design specification the feature images on the Technology page need to change based on the window size.
Desktop image uncropped:
Tablet image cropped:
Phone image cropped:
I brainstormed three different implementation patterns:
i. Google says write custom hooks for media queries then point img src at a template string generated based on the numbered tab state and media query.
const [tabNumber, setTabNumber] = useState(1);
const query = useMediaQuery('desktop');
const imageClasses = {
1: 'launch-vehicle',
2: 'spaceport',
3: 'space-capsule',
};
let imgUrl = `/src/assets/technology/image-${tabTitle}-${query}.jpg`;
...
<img src={imgUrl} alt={technology[tabNumber - 1].name} />ii. Give img a className based on a dictionary with keys of the numbered tab state, then load images in CSS by using the content attr and media queries
const imageClasses = {
1: 'img-launch',
2: 'img-port',
3: 'img-capsule',
};
...
<img
className={imageClasses[tabNumber]}
alt={technology[tabNumber - 1].name}
/>;.img-launch {
content: url('/src/assets/technology/image-launch-vehicle-landscape.jpg');
}
@media (min-width: 45em) {
.img-launch {
content: url('/src/assets/technology/image-launch-vehicle-portrait.jpg');
}
}iii. Another option would be to only use the larger uncropped images, but using the media query hook created in (i.) the image could be cropped and resized.
I decided to go for the second option as this is the least amount of work to implement. In terms of performance the first option might be the better pattern since you save a render. On this small site it is negligible, but at larger scale this would be one path to investigate for extra performance.
- Importing images to display in React
Complex JavaScript web apps use bundlers to "build" the app so that it is able to be deployed and run in a browser. If using a build tool like webpack (Vite uses Rollup to bundle during build), then one step of bundling is processing all imported assets in each React component.
For example, instead of explicitly specifying the path to an image file in the JSX of a component (like <img src="../assets/logo.svg" alt="logo" />), assets (generally) need to be explicitly imported in the first lines of a component, and then the imported asset can then be referenced by JSX.
import { Logo } from '../assets/logo.svg';
export function Header() {
...
return (
<main>
<img src={Logo} alt='logo' />
...
</main>
)
}-
Utility classes are nontrivial to set up, but the benefit is obvious for maintainable cohesive design. I think Tailwind CSS could allow for much faster design iteration and would be worth looking into.
-
Refactor site from CSR to SSG
- This is a Client Side Rendered (CSR) React app. They are the simplest to set up, but are Not Great in terms of performance (due to the rendering time on the client's browser) and are bad for Search Engine Optimization (SEO). CSR apps are bad for SEO because search engine crawlers can only initially see the config
index.htmlfile and have to wait for React to render. - Since this website only contains static, unchanging pages (no user or dynamic information is fetched and displayed), that means this could be refactored to be a Static Site Generated (SSG) application. SSG apps are more performant and better for SEO since they are just static pages rendered at build time. Static sites are also cheaper to host.
- There is a Vite plugin which allows for easy SSG and would be an excellent next step for development.
- This is a Client Side Rendered (CSR) React app. They are the simplest to set up, but are Not Great in terms of performance (due to the rendering time on the client's browser) and are bad for Search Engine Optimization (SEO). CSR apps are bad for SEO because search engine crawlers can only initially see the config
-
Set up and hook into a CMS (Content Management System) such as Sanity for site content, instead of querying a data.json file. Using a CMS is often best for easily scaling and managing content that changes over time for a web app.
- Optionally: Use the npm package
localforageto create a fake interface to a CMS like in this React Router example. This would be a good step for a learning project without worrying about database setup.
- Optionally: Use the npm package
-
Lazy loading images useful guide
- For pages with tabs with multiple images, all the images are downloaded by default on page load. Lazy loading allows for images to be downloaded and shown only when the user makes them visible on screen. This saves bandwidth costs and improves initial page load.
-
Serve appropriate image size using
srcset- Instead of using multiple CSS media queries, the
srcsetHTML attribute allows the developer to define multiple image formats that can be served. Mozilla documentation. Not sure if this is better or worse than using media queries.
- Instead of using multiple CSS media queries, the
- Reduce rendering load of animated box shadows by 10x - Using transform on an element's box-shadow is extremely compute intensive. Using transform on that element's
::afterpsuedoelement instead (to create a hover outline effect) is better practice.
- Twitter - @0x_eddie
TODO




