Als je zoals ik bent en je houdt van sneltoetsen, weet je hoe bevredigend het is om een paar toetsen in te drukken en de magie te zien gebeuren. Of het nu de bekende Ctrl+C ā Ctrl+V is die ontwikkelaars gebruiken om “code te lenen” š van LLMs en codepagina’s, of de gepersonaliseerde sneltoetsen die we instellen in onze favoriete tools, sneltoetsen besparen tijd en laten ons voelen als een computerwizard.
Maak je geen zorgen! Ik heb de code gekraakt voor het bouwen van componenten die sneltoetsen activeren en erop reageren. In dit artikel leer ik je hoe je ze kunt maken met React, Tailwind CSS en Framer Motion.
Inhoudsopgave
Hier is alles wat we zullen behandelen:
Vereisten
-
Basisprincipes van HTML, CSS en Tailwind CSS
-
Basisprincipes van JavaScript, React en React Hooks.
Wat is een Keyboard Shortcut Listener (KSL) Component?
Een Toetsenbord Sneltoets Luistercomponent (KSLC) is een component die luistert naar specifieke toetscombinaties en acties in je app activeert. Het is ontworpen om je app te laten reageren op sneltoetsen, waardoor een soepelere en efficiƫntere gebruikerservaring ontstaat.
Waarom is het belangrijk?
-
Toegankelijkheid: De KSL-component maakt het eenvoudig voor mensen die een toetsenbord gebruiken om acties te activeren, waardoor je app inclusiever en gebruiksvriendelijker wordt.
-
Snellere Ervaring: Sneltoetsen zijn snel en efficiĆ«nt, waardoor gebruikers dingen in minder tijd gedaan krijgen. Geen gedoe meer met de muisādruk gewoon op een toets (of twee) en hop, actie gebeurt!
-
Herbruikbaarheid: Zodra je je KSL hebt ingesteld, kan deze verschillende sneltoetsen in je app beheren, waardoor het eenvoudig is om toe te voegen zonder dezelfde logica opnieuw te schrijven.
-
Nettere Code: In plaats van toetsenbordgebeurtenisluisteraars overal te verspreiden, houdt de KSL-component de boel netjes door de logica te centraliseren. Je code blijft schoon, georganiseerd en gemakkelijker te onderhouden.
Hoe de KSL-component te bouwen
Ik heb een GitHub-repository voorbereid met startbestanden om de zaken te versnellen. Clone gewoon deze repo en installeer de afhankelijkheden.
Voor dit project gebruiken we de startpagina van Tailwind als onze inspiratie en creƫren we de KSL-functionaliteit. Na het installeren en uitvoeren van de build-opdracht, zou je pagina er als volgt uit moeten zien:
Hoe de Reveal-component te maken
De reveal-component is de component die we willen tonen wanneer we de sneltoets gebruiken.
Om te beginnen, maak een bestand genaamd search-box.tsx
en plak deze code erin:
export default function SearchBox() {
return (
<div className="fixed top-0 left-0 w-full h-full backdrop-blur-sm bg-slate-900/50 ">
{" "}
<div className=" p-[15vh] text-[#939AA7] h-full">
<div className="max-w-xl mx-auto divide-y divide-[#939AA7] bg-[#1e293b] rounded-md">
<div className="relative flex justify-between px-4 py-2 text-sm ">
<div className="flex items-center w-full gap-2 text-white">
<BiSearch size={20} />
<input
type="text"
className="w-full h-full p-2 bg-transparent focus-within:outline-none"
placeholder="Search Documentation"
/>
</div>
<div className="absolute -translate-y-1/2 right-4 top-1/2 ">
<kbd className="p-1 text-xs rounded-[4px] bg-[#475569] font-sans font-semibold text-slate-400">
<abbr title="Escape" className="no-underline ">
Esc{" "}
</abbr>{" "}
</kbd>
</div>
</div>
<div className="flex items-center justify-center p-10 text-center ">
<h2 className="text-xl">
How many licks does it take to get to the center of a Tootsie pop?
</h2>
</div>
</div>
</div>
</div>
);
}
OkƩ, wat gebeurt er in deze code?
-
Hoofd Overlay (
<div className="fixed top-0 left-0 ...">
)-
Dit is de overlay op volledig scherm die de achtergrond dimt.
-
De
backdrop-blur-sm
voegt een subtiele vervaging aan de achtergrond toe, enbg-slate-900/50
geeft het een semi-doorzichtige donkere overlay.
-
-
Zoekbalkomslag (
<div className="p-[15vh] ...">
)-
De inhoud is gecentreerd met behulp van padding en flex-utilities.
-
De
max-w-xl
zorgt ervoor dat de zoekbalk binnen een redelijke breedte blijft voor leesbaarheid.
-
Maak dan in je App.tsx
een staat die dat component dynamisch toont:
const [isOpen, setIsOpen] = useState<boolean>(false);
-
useState
: Deze hook initialiseertisOpen
opfalse
, wat betekent dat de zoekbalk standaard verborgen is. -
Wanneer
isOpen
is ingesteld optrue
, zal deSearchBox
component op het scherm worden weergegeven.
En render de zoekcomponent:
{isOpen && <SearchBox />}
Om de zoekcomponent te tonen, voeg een schakel functie toe aan de invoerknop:
<button
type="button"
className="items-center hidden h-12 px-4 space-x-3 text-left rounded-lg shadow-sm sm:flex w-72 ring-slate-900/10 focus:outline-none hover:ring-2 hover:ring-sky-500 focus:ring-2 focus:ring-sky-500 bg-slate-800 ring-0 text-slate-300 highlight-white/5 hover:bg-slate-700"
onClick={() => setIsOpen(true)}>
<BiSearch size={20} />
<span className="flex-auto">Quick search...</span>
<kbd className="font-sans font-semibold text-slate-500">
<abbr title="Control" className="no-underline text-slate-500">
Ctrl{" "}
</abbr>{" "}
K
</kbd>
</button>
De onClick
gebeurtenis stelt isOpen
in op true
, waardoor de SearchBox
zichtbaar wordt.
Maar zoals je hebt gezien, werd dit getriggerd door een klikactie, niet door een sneltoetsactie. Laten we dat nu doen.
Hoe de component via sneltoets te triggeren
Om de onthulcomponent te openen en sluiten met een sneltoets, gebruiken we een useEffect
hook om te luisteren naar specifieke toetscombinaties en de status van de component dienovereenkomstig bij te werken.
Stap 1: Luister naar Toetsgebeurtenissen
Voeg een useEffect
hook toe in je App.tsx
bestand om te luisteren naar toetsaanslagen:
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.ctrlKey && event.key === Key.K) {
event.preventDefault(); // Voorkom standaard browsergedrag
} };
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, []);
Wat gebeurt er in deze code?
-
Effect Setup (
useEffect
)useEffect
zorgt ervoor dat de gebeurtenisluisteraar voor toetsaanslagen wordt toegevoegd wanneer de component wordt gemonteerd en wordt opgeruimd wanneer de component wordt ontmanteld, waardoor geheugenlekken worden voorkomen.
-
Toetscombinatie (
event.ctrlKey && event.key === "k"
)-
De
event.ctrlKey
controleert of de Control-toets wordt ingedrukt. -
De
event.key === "k"
zorgt ervoor dat we specifiek luisteren naar de “K”-toets. Samen controleert dit of de Ctrl + K combinatie is ingedrukt.
-
-
Voorkom Standaardgedrag (
event.preventDefault()
)- Sommige browsers hebben standaardgedragingen gekoppeld aan toetscombinaties zoals Ctrl + K (bijv. het focussen op de adresbalk van de browser). Het aanroepen van
preventDefault
stopt dit gedrag.
- Sommige browsers hebben standaardgedragingen gekoppeld aan toetscombinaties zoals Ctrl + K (bijv. het focussen op de adresbalk van de browser). Het aanroepen van
-
Evenement Opruiming (
return () => ...
)- De opruimfunctie verwijdert de gebeurtenislistener om te voorkomen dat er dubbele listeners worden toegevoegd als de component opnieuw wordt gerenderd.
Stap 2: Wissel Componentzichtbaarheid
Vervolgens, werk de handleKeyDown
functie bij om de zichtbaarheid van de SearchBox
te wisselen wanneer de sneltoets wordt ingedrukt:
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// Luister naar Ctrl + K
if (event.ctrlKey && event.key === Key.K) {
event.preventDefault(); // Voorkom standaardgedrag van de browser
setIsOpen((prev) => !prev); // Wissel de zoekbalk
} else if (event.key === Key.Escape) {
setIsOpen(false); // Sluit de zoekbalk
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, []);
Wat gebeurt er in deze code?
-
Toestand Wisselen (
setIsOpen((prev) => !prev)
)-
Wanneer Ctrl + K wordt ingedrukt, wisselt de
setIsOpen
state setter de zichtbaarheid van deSearchBox
. -
Het
prev
argument vertegenwoordigt de vorige toestand. Het gebruik van!prev
draait de waarde om:-
true
(open) wordtfalse
(sluiten). -
false
(gesloten) wordttrue
(open).
-
-
-
Sluiten met de Escape-toets (
event.key === "Escape"
)- Wanneer de Escape-toets wordt ingedrukt, stelt
setIsOpen(false)
expliciet de status in opfalse
, wat deSearchBox
sluit.
- Wanneer de Escape-toets wordt ingedrukt, stelt
Dit resulteert in het volgende:
Hoe de zichtbaarheid van de component te animeren
Op dit moment werkt onze component, maar het mist een beetje flair, vind je niet? Laten we dat veranderen.
Stap 1: Maak de Overlay-component
We beginnen met het maken van een overlaycomponent, die fungeert als de donkere, vervaagde achtergrond voor de zoekbox. Hier is de basisversie:
import { ReactNode } from "react";
export default function OverlayWrapper({ children }: { children: ReactNode }) {
return (
<div
className="fixed top-0 left-0 w-full h-full backdrop-blur-sm bg-slate-900/50 ">
{children}
</div>
);
}
Stap 2: Voeg animaties toe aan de Overlay
Laten we nu de overlay laten in- en uitfaden met Framer Motion. Werk de OverlayWrapper
-component als volgt bij:
import { motion } from "framer-motion";
import { ReactNode } from "react";
export default function OverlayWrapper({ children }: { children: ReactNode }) {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed top-0 left-0 w-full h-full backdrop-blur-sm bg-slate-900/50 ">
{children}
</motion.div>
);
}
Belangrijke animatie-eigenschappen:
-
initial
: Stelt de beginstatus in wanneer de component is gemonteerd (volledig transparant). -
animate
: Definieert de status waarnaar geanimeerd moet worden (volledig ondoorzichtig). -
exit
: Specificeert de animatie wanneer het component wordt verwijderd (uitfaden).
Stap 3: Animeer het zoekvak
Voeg vervolgens wat beweging toe aan het zoekvak zelf. We laten het schuiven en vervagen wanneer het verschijnt en uitschuiven wanneer het verdwijnt.
import { motion } from "framer-motion";
import { BiSearch } from "react-icons/bi";
import OverlayWrapper from "./overlay";
export default function SearchBox() {
return (
<OverlayWrapper>
<motion.div
initial={{ y: "-10%", opacity: 0 }}
animate={{ y: "0%", opacity: 1 }}
exit={{ y: "-5%", opacity: 0 }}
className=" p-[15vh] text-[#939AA7] h-full">
<div
className="max-w-xl mx-auto divide-y divide-[#939AA7] bg-[#1e293b] rounded-md"
>
<div className="relative flex justify-between px-4 py-2 text-sm ">
<div className="flex items-center w-full gap-2 text-white">
<BiSearch size={20} />
<input
type="text"
className="w-full h-full p-2 bg-transparent focus-within:outline-none"
placeholder="Search Documentation"
/>
</div>
<div className="absolute -translate-y-1/2 right-4 top-1/2 ">
<kbd className="p-1 text-xs rounded-[4px] bg-[#475569] font-sans font-semibold text-slate-400">
<abbr title="Escape" className="no-underline ">
Esc{" "}
</abbr>{" "}
</kbd>
</div>
</div>
<div className="flex items-center justify-center p-10 text-center ">
<h2 className="text-xl">
How many licks does it take to get to the center of a Tootsie pop?
</h2>
</div>
</div>
</motion.div>
</OverlayWrapper>
);
}
Stap 4: Activeer Animatietracking met AnimatePresence
Omring tot slot je voorwaardelijke renderlogica met het AnimatePresence
-component geleverd door Framer Motion. Dit zorgt ervoor dat Framer Motion bijhoudt wanneer elementen de DOM binnenkomen en verlaten.
<AnimatePresence>{isOpen && <SearchBox />}</AnimatePresence>
Dit stelt Framer Motion in staat om bij te houden wanneer een element de DOM binnenkomt en verlaat. Hiermee krijgen we het volgende resultaat:
Ah, veel beter!
Hoe je jouw KSL-component optimaliseert
Als je dacht dat we klaar waren, niet zo snel… We hebben nog wat meer te doen.
We moeten optimaliseren voor toegankelijkheid. We moeten een manier toevoegen voor gebruikers om het zoekcomponent met een muis te sluiten, aangezien toegankelijkheid erg belangrijk is.
Begin hiermee door een hook genaamd useClickOutside
te maken. Deze hook gebruikt een referentie-element om te weten wanneer een gebruiker buiten het doelelement klikt (zoekvak), wat een zeer populair gedrag is om modals en KSLC’s te sluiten.
import { useEffect } from "react";
type ClickOutsideHandler = (event: Event) => void;
export const useClickOutside = (
ref: React.RefObject<HTMLElement>,
handler: ClickOutsideHandler
) => {
useEffect(() => {
const listener = (event: Event) => {
// Doe niets als er wordt geklikt op het element van de referentie of afstammelingen
if (!ref.current || ref.current.contains(event.target as Node)) return;
handler(event);
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, [ref, handler]);
};
Om deze hook te gebruiken, geef de functie door die verantwoordelijk is voor het openen en sluiten van het zoekcomponent:
<AnimatePresence> {isOpen && <SearchBox close={setIsOpen} />} </AnimatePresence>
Ontvang dan de functie in de zoekopdracht met het juiste prop-type:
export default function SearchBox({
close,
}: {
close: React.Dispatch<React.SetStateAction<boolean>>;
}) {
Daarna maak je een referentie (ref) naar het item dat je wilt volgen en markeer je dat element:
import { motion } from "framer-motion";
import { useRef } from "react";
import { BiSearch } from "react-icons/bi";
import { useClickOutside } from "../hooks/useClickOutside";
import OverlayWrapper from "./overlay";
export default function SearchBox({
close,
}: {
close: React.Dispatch<React.SetStateAction<boolean>>;
}) {
const searchboxRef = useRef<HTMLDivElement>(null);
return (
<OverlayWrapper>
<motion.div
initial={{ y: "-10%", opacity: 0 }}
animate={{ y: "0%", opacity: 1 }}
exit={{ y: "-5%", opacity: 0 }}
className=" p-[15vh] text-[#939AA7] h-full">
<div
className="max-w-xl mx-auto divide-y divide-[#939AA7] bg-[#1e293b] rounded-md"
ref={searchboxRef}>
<div className="relative flex justify-between px-4 py-2 text-sm ">
<div className="flex items-center w-full gap-2 text-white">
<BiSearch size={20} />
<input
type="text"
className="w-full h-full p-2 bg-transparent focus-within:outline-none"
placeholder="Search Documentation"
/>
</div>
<div className="absolute -translate-y-1/2 right-4 top-1/2 ">
<kbd className="p-1 text-xs rounded-[4px] bg-[#475569] font-sans font-semibold text-slate-400">
<abbr title="Escape" className="no-underline ">
Esc{" "}
</abbr>{" "}
</kbd>
</div>
</div>
<div className="flex items-center justify-center p-10 text-center ">
<h2 className="text-xl">
How many licks does it take to get to the center of a Tootsie pop?
</h2>
</div>
</div>
</motion.div>
</OverlayWrapper>
);
}
Geef vervolgens die ref en de functie door die moet worden aangeroepen wanneer er een klik buiten dat element wordt gedetecteerd.
useClickOutside(searchboxRef, () => close(false));
Het nu testen geeft het volgende resultaat:
We kunnen de code ook nog iets verder optimaliseren. Zoals we deden met de toegankelijkheidsfunctie, kunnen we onze listener voor het detecteren van sneltoetsen veel schoner en efficiƫnter maken met de volgende stappen.
Eerst maak je een useKeyBindings
hook-bestand voor het afhandelen van toetscombinaties.
Definieer vervolgens de hook en de interface. De hook accepteert een array van bindings, waarbij elke binding bestaat uit:
-
Een
keys
array, die de toetscombinatie specificeert (bijvoorbeeld, [“Control”, “k”]) -
Een callbackfunctie, die wordt aangeroepen wanneer de bijbehorende toetsen worden ingedrukt.
import { useEffect } from "react";
// Definieer de structuur van een toetsbinding
interface KeyBinding {
keys: string[]; // Array van toetsen (bijv. ["Control", "k"])
callback: () => void; // Functie die moet worden uitgevoerd wanneer de toetsen worden ingedrukt
}
export const useKeyBindings = (bindings: KeyBinding[]) => {
};
Maak vervolgens de handleKeyDown
functie aan. Binnen de hook definieer je een functie die luistert naar toetsenbordgebeurtenissen. Deze functie controleert of de ingedrukte toetsen overeenkomen met gedefinieerde toetscombinaties.
We zullen de toetsen normaliseren naar kleine letters zodat de vergelijking niet hoofdlettergevoelig is en bijhouden welke toetsen zijn ingedrukt door te controleren op ctrlKey
, shiftKey
, altKey
, metaKey
en de ingedrukte toets (bijvoorbeeld “k” voor Ctrl + K).
const handleKeyDown = (event: KeyboardEvent) => {
// Houd de ingedrukte toetsen bij
const pressedKeys = new Set<string>();
// Controleer op modifier-toetsen (Ctrl, Shift, Alt, Meta)
if (event.ctrlKey) pressedKeys.add("control");
if (event.shiftKey) pressedKeys.add("shift");
if (event.altKey) pressedKeys.add("alt");
if (event.metaKey) pressedKeys.add("meta");
// Voeg de ingedrukte toets toe (bijv. "k" voor Ctrl + K)
if (event.key) pressedKeys.add(event.key.toLowerCase());
};
Vervolgens vergelijken we de ingedrukte toetsen met de toetsenarray van onze bindings om te controleren of ze overeenkomen. Als dat het geval is, roepen we de bijbehorende callbackfunctie aan. We zorgen er ook voor dat het aantal ingedrukte toetsen overeenkomt met het aantal toetsen dat in de binding is gedefinieerd.
// Loop door elke keybinding
bindings.forEach(({ keys, callback }) => {
// Normalize de toetsen naar kleine letters voor vergelijking
const normalizedKeys = keys.map((key) => key.toLowerCase());
// Controleer of de ingedrukte toetsen overeenkomen met de keybinding
const isMatch =
pressedKeys.size === normalizedKeys.length &&
normalizedKeys.every((key) => pressedKeys.has(key));
// Als de toetsen overeenkomen, roep de callback aan
if (isMatch) {
event.preventDefault(); // Voorkom het standaard browser gedrag
callback(); // Voer de callbackfunctie uit
}
});
Stel tot slot gebeurtenisluisteraars in op het window object om te luisteren naar keydown-gebeurtenissen. Deze luisteraars zullen de handleKeyDown
functie activeren telkens wanneer een toets wordt ingedrukt. Zorg ervoor dat de gebeurtenisluisteraars worden opgeschoond wanneer het component wordt verwijderd.
useEffect(() => {
// Voeg gebeurtenisluisteraars toe voor keydown
window.addEventListener("keydown", handleKeyDown);
// Maak de gebeurtenisluisteraars schoon wanneer het component wordt verwijderd
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [bindings]);
De volledige useKeyBindings
hook ziet er nu als volgt uit:
import { useEffect } from "react";
interface KeyBinding {
keys: string[]; // Een combinatie van toetsen om de callback te activeren (bijv. ["Control", "k"])
callback: () => void; // De functie die moet worden uitgevoerd wanneer de toetsen worden ingedrukt
}
export function useKeyBindings(bindings: KeyBinding[]) {
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
bindings.forEach(({ keys, callback }) => {
const normalizedKeys = keys.map((key) => key.toLowerCase());
const pressedKeys = new Set<string>();
// Modificatietoetsen expliciet bijhouden
if (event.ctrlKey) pressedKeys.add("control");
if (event.shiftKey) pressedKeys.add("shift");
if (event.altKey) pressedKeys.add("alt");
if (event.metaKey) pressedKeys.add("meta");
// Voeg de daadwerkelijk ingedrukte toets toe
if (event.key) pressedKeys.add(event.key.toLowerCase());
// Exact overeenkomen: ingedrukte toetsen moeten overeenkomen met de gedefinieerde toetsen
const isExactMatch =
pressedKeys.size === normalizedKeys.length &&
normalizedKeys.every((key) => pressedKeys.has(key));
if (isExactMatch) {
event.preventDefault(); // Standaardgedrag voorkomen
callback(); // Voer de callback uit
}
});
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [bindings]);
}
Zo kun je deze hook gebruiken in je App
:
import { useKeyBindings } from "./hooks/useKeyBindings";
export default function App() {
const [isOpen, setIsOpen] = useState<boolean>(false);
useKeyBindings([
{
keys: ["Control", "k"], // Luister naar "Ctrl + K"
callback: () => setIsOpen((prev) => !prev), // Schakel het zoekvak in/uit
},
{
keys: ["Escape"], // Luister naar "Escape"
callback: () => setIsOpen(false), // Sluit het zoekvak
},
]);
Dit resulteert in het volgende:
Met deze aanpak kun je zelfs meerdere sneltoetsen toevoegen om de zichtbaarheid van de zoekcomponent te activeren.
useKeyBindings([
{
keys: ["Control", "k"], // Luister naar "Ctrl + K"
callback: () => setIsOpen((prev) => !prev), // Wissel de zoekbalk om
},
{
keys: ["Control", "d"], // Luister naar "Ctrl + D"
callback: () => setIsOpen((prev) => !prev), // Wissel de zoekbalk om
},
{
keys: ["Escape"], // Luister naar "Escape"
callback: () => setIsOpen(false), // Sluit de zoekbalk
},
]);
Hier zijn links naar alle bronnen die je mogelijk nodig hebt voor dit artikel:
Conclusie
Ik hoop dat dit artikel aanvoelde als een goed getimede sneltoets, die je naar de kern van het bouwen van herbruikbare toetsenbord-sneltoetscomponenten leidde. Met elke toetsaanslag en animatie kun je nu gewone webervaringen buitengewoon maken.
Ik hoop dat je sneltoetsen je helpen om apps te creƫren die aanslaan bij je gebruikers. Tenslotte beginnen de beste reizen vaak met precies de juiste combinatie.
Vind je mijn artikelen leuk?
Voel je vrij om hier een koffie voor me te kopen, om mijn brein te laten werken en meer artikelen zoals deze te kunnen maken.
Contactgegevens
Wil je contact met me opnemen? Aarzel niet om me te bereiken via de volgende kanalen:
-
Twitter / X: @jajadavid8
-
LinkedIn: David Jaja
-
Email: [email protected]