Skip to content

You may not need that npm package

Posted on:April 6, 2023

After installing the Webpack Bundle Analyzer on a few of my projects I was blown away how heavy some of these, seemingly simple, packages can be. So before immediately reaching for an npm package try to see if you can build the feature yourself. You may find the end result to be well worth your time, ending with a component that is not a only a fraction of the size but also tailored to fit your specific needs. Let’s build a few components together!

Tooltip Component

I needed a simple tooltip component so I initially reached for react-tooltip. I ran into some issues with it though in Astro/mdx files and also I really only need a fraction of it’s features. It’s pretty gnarly that a simple tooltip package is like 45kb minified. So anyways I decided to build my own. I started by looking at the WAI-ARIA Authoring Practices for tooltips.

import React, { useState } from "react";
import useUniqueId from "../utils/useUniqueId";

type TooltipPros = {
	content: string,
	children: React.ReactElement,
};

const Tooltip: React.FC<TooltipPros> = ({ content, children }) => {
	const [show, setShow] = useState(false);
	const tooltipId = useUniqueId("tooltip");

	const handleMouseEnter = () => {
		setShow(true);
	};

	const handleMouseLeave = () => {
		setShow(false);
	};

	const handleFocus = () => {
		setShow(true);
	};

	const handleBlur = () => {
		setShow(false);
	};

	return (
		<span
			className="tooltip-container"
			onMouseEnter={handleMouseEnter}
			onMouseLeave={handleMouseLeave}
			onFocus={handleFocus}
			onBlur={handleBlur}
			tabIndex={0}
			aria-describedby={tooltipId}
		>
			{children}
			{show && (
				<span className="tooltip-content" id={tooltipId} role="tooltip">
					{content}
				</span>
			)}
		</span>
	);
};

export default Tooltip;

Pretty straight forward, we got a few spans for the tooltip and the tooltips’ content. We got a single useState to keep track of the tooltip’s state. We then add event listeners to the container to show/hide the tooltip. For accessibility I added the tabIndex to the container so that it can be focused on as well as an aria-describedby attribute to the container so that screen readers know that the tooltip content is related to the container. We also add a role attribute to the tooltip content so that screen readers know that it is a tooltip. Finally we add an id attribute to the tooltip content so that it can be referenced by the aria-describedby attribute on the container.

and here’s the css:

/* how you want your tooltip link to look */
.tooltip-container {
	position: relative;
	display: inline-block;
	cursor: default;
	color: var(--purple-light);
}

.tooltip-content {
	position: absolute;
	bottom: 100%;
	left: 50%;
	transform: translateX(-50%);
	padding: 5px 7px;
	background-color: rgb(0 0 0 / 50%);
	color: #fff;
	font-size: 0.875rem;
	border-radius: 4px;
	white-space: nowrap;
	pointer-events: none;
	opacity: 0;
	transition: all 0.3s ease-in-out;
}

.tooltip-container:hover .tooltip-content {
	opacity: 1;
}

Usage:

<Tooltip content="🎉 This is a tooltip!" title="Hover over me!" />

That’s all there is to it, here’s an

example of how it looks. Not too bad, eh?

Accordion Component

Another common piece of UI is an accordion or expand/collapse component. These are pretty simple to build, here’s an example of one I built recently:

import { useState } from "react";
import useUniqueId from "../utils/useUniqueId";

const Accordion = ({
	heading,
	content,
}: {
	heading: string,
	content: string,
}) => {
	const [isActive, setIsActive] = useState(false);
	const panelId = useUniqueId("accordion");

	return (
		<div className="accordion">
			<h3>
				<button
					id={panelId}
					aria-controls={panelId}
					aria-expanded={isActive}
					onClick={() => setIsActive(!isActive)}
					className="accordion-toggle"
				>
					{heading}
					<i className={`chevron-down ${isActive ? "open" : ""}`} />
				</button>
			</h3>
			<p
				id={panelId}
				role="region"
				aria-labelledby={panelId}
				className={`accordion-content ${isActive ? "active" : ""}`}
			>
				{content}
			</p>
		</div>
	);
};

export default Accordion;

and the css:

.accordion-toggle {
	cursor: pointer;
	display: flex;
	flex-direction: row;
	justify-content: space-between;
	align-items: center;
	width: 100%;
	margin: 0;
	background: rgba(0 0 0 / 20%);
	border-radius: 7px;
	padding: 8px 15px;
}

.accordion-content {
	max-height: 0;
	opacity: 0;
	transform: translate(0, -15px);
	overflow: hidden;
	transition: all 0.3s ease;
}
.accordion-content.active {
	opacity: 1;
	transform: translate(0);
	max-height: 100rem;
}

.chevron-down {
	border: 2px solid transparent;
	transition: all 0.3s ease-in-out;
}

.chevron-down.open {
	transform: rotate(180deg) translateX(-4px);
}

.chevron-down::after {
	content: "";
	display: block;
	box-sizing: border-box;
	width: var(--chevron-size);
	height: var(--chevron-size);
	border-bottom: 1px solid;
	border-right: 1px solid;
	transform: rotate(45deg) translateY(-4px);
	transform-origin: center;
}

Usage:

<Accordion heading="Heading is here" content="Content is here" />

And here it is in action:

Tony Iommi 🎸🤘🧙🏻‍♂️⚡️

Wrapping up

Hope this helps inspire you to maybe create your own components or something, and they’re not too difficult to build. If not umm here have a random kitten pic:

Edit on Github
More Posts