Adding a Homepage Skills Carousel
Overview
This KB explains how to add a dynamic "Skills" (or "Tools") carousel section to the homepage, using images from a folder. The section is easy to maintain, just add or remove images in the icons folder, and the carousel updates automatically.
Implementation
Prepare the Icons
The skill/tool icons (PNG, SVG, JPG, etc.) are placed in assets/site-design/tools-skills/icons.
Newer images can be added at any time without code changes, just drop them in the folder.
Create the Skills Component
File: src/components/homepage/Skills.tsx
The require.context (Webpack) is used to dynamically load all images from the folder without manual imports. No hardcoded filenames, which means new images are picked up automatically.
View code
import React, { FunctionComponent, useMemo } from "react";
import styles from "./Skills.module.scss";
type SkillImage = {
src: string;
alt: string;
};
export const Skills: FunctionComponent = () => {
const skillImages = useMemo<SkillImage[]>(() => {
const imagesContext = (require as any).context(
"../../../assets/site-design/tools-skills/icons",
false,
/\.[^/.]+$/i
);
return imagesContext
.keys()
.sort((first: string, second: string) =>
first.localeCompare(second, undefined, { numeric: true, sensitivity: "base" })
)
.map((imagePath: string) => {
const source = imagesContext(imagePath) as any;
const normalizedSource =
typeof source === "string"
? source
: typeof source?.default === "string"
? source.default
: source?.default?.src || source?.src || "";
return {
src: normalizedSource,
alt: imagePath
.replace("./", "")
.replace(/\.[^/.]+$/, "")
.replace(/[-_]+/g, " "),
};
})
.filter((skillImage: SkillImage) => Boolean(skillImage.src));
}, []);
return (
<section className={styles.skillsSection} aria-label="Skills">
<h2 className={styles.skillsTitle}>SKILLS</h2>
<p className={styles.skillsIntro}>
A selection of technologies, platforms, and tools I use regularly.
</p>
<div className={styles.skillsCarouselSection}>
<div className={styles.skillsCarouselShell}>
<div className={styles.skillsCarouselViewport}>
<div className={styles.skillsCarouselTrackContinuous}>
{[...skillImages, ...skillImages].map((skillImage: SkillImage, idx: number) => (
<div className={styles.skillsCarouselSlide} key={`${skillImage.src}-${idx}`}>
<img
src={skillImage.src}
alt={idx < skillImages.length ? skillImage.alt : ""}
className={styles.skillsCarouselImage}
loading="lazy"
aria-hidden={idx >= skillImages.length}
/>
</div>
))}
</div>
</div>
</div>
</div>
</section>
);
};
Styling
File: src/components/homepage/Skills.module.scss
The dedicated SCSS file contains all styles (width, carousel animation, dark mode, etc.).
Example class names:
skillsSectionskillsTitleskillsCarouselImage, etc.
Add to Homepage
File: src/pages/index.tsx
As a final step, import and render the <Skills /> component in the TSX file for the homepage (e.g., after the Experiences section).
View code
import { Skills } from "../components/homepage/Skills";
export default function Home(): JSX.Element {
const { siteConfig } = useDocusaurusContext();
return (
<Layout title="Home" description={siteConfig.tagline}>
<main className="homepage">
<Hero />
<Experiences />
<Skills />
</main>
</Layout>
);
}
Maintenance
To add a new skill/tool, just drop a new image in the icons folder, no code changes needed.
The carousel now reads any file extension with this regex:
/\.[^/.]+$/i
This means you do not need to maintain a list of supported image formats.
The file order is sorted with numeric-aware sorting, so prefixed filenames like 1-, 2-, 10- load in expected order.
Notes:
- This approach is future-proof for Docusaurus/React+Webpack projects.
- For Vite or other bundlers, use their dynamic import features instead.