Se você é como eu e adora atalhos, sabe o quão satisfatório é pressionar algumas teclas e ver a mágica acontecer. Seja o familiar Ctrl+C – Ctrl+V que os desenvolvedores usam para “pegar emprestado código” 😉 de LLMs e páginas de código, ou os atalhos personalizados que configuramos em nossas ferramentas favoritas, os atalhos de teclado economizam tempo e nos fazem sentir como um gênio da computação.
Bem, não tema! Eu decifrei o código para construir componentes que acionam e respondem a atalhos de teclado. Neste artigo, vou te ensinar como criá-los com React, Tailwind CSS e Framer Motion.
Tabela de Conteúdo
Aqui está tudo que vamos abordar:
Pré-requisitos
-
Fundamentos de HTML, CSS e Tailwind CSS
-
Fundamentos de JavaScript, React e React Hooks.
O Que É um Componente de Listener de Atalho de Teclado (KSL)?
Um Componente Ouvinte de Atalhos de Teclado (KSLC) é um componente que escuta combinações de teclas específicas e aciona ações no seu aplicativo. Ele é projetado para fazer seu aplicativo responder a atalhos de teclado, permitindo uma experiência de usuário mais suave e eficiente.
Por que isso é importante?
-
Acessibilidade: O componente KSL torna simples para as pessoas que usam um teclado acionarem ações, tornando seu aplicativo mais inclusivo e fácil de usar.
-
Experiência Mais Ágil: Os atalhos são rápidos e eficientes, permitindo que os usuários realizem tarefas em menos tempo. Nada de procurar o mouse—basta pressionar uma tecla (ou duas) e pronto, a ação acontece!
-
Reutilização: Depois de configurar seu KSL, ele pode lidar com diferentes atalhos em todo o seu aplicativo, tornando fácil adicionar sem reescrever a mesma lógica.
-
Código mais Limpo: Em vez de espalhar ouvintes de eventos de teclado por toda parte, o componente KSL mantém as coisas organizadas centralizando a lógica. Seu código permanece limpo, organizado e mais fácil de manter.
Como Construir o Componente KSL
Preparei um repositório no GitHub com arquivos iniciais para acelerar o processo. Basta clonar este repositório e instalar as dependências.
Para este projeto, estamos usando a página inicial do Tailwind como inspiração e criando a funcionalidade KSL. Após instalar e executar o comando de compilação, veja como sua página deve ficar:
Como Criar o Componente de Revelação
O componente de revelação é o componente que queremos mostrar quando usarmos o atalho.
Para começar, crie um arquivo chamado search-box.tsx
e cole este código:
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, então o que está acontecendo neste código?
-
Sobreposição Principal (
<div className="fixed top-0 left-0 ...">
)-
Esta é a sobreposição em tela cheia que escurece o plano de fundo.
-
O
backdrop-blur-sm
adiciona um leve desfoque ao plano de fundo, ebg-slate-900/50
dá a ele uma sobreposição escura semitransparente.
-
-
Wrapper da Caixa de Pesquisa (
<div className="p-[15vh] ...">
)-
O conteúdo é centralizado usando padding e utilitários de flex.
-
A
max-w-xl
garante que a caixa de pesquisa permaneça dentro de uma largura razoável para legibilidade.
-
Então, no seu App.tsx
, crie um estado que mostra dinamicamente esse componente:
const [isOpen, setIsOpen] = useState<boolean>(false);
-
useState
: Este hook inicializaisOpen
comofalse
, significando que a caixa de pesquisa está oculta por padrão. -
Quando
isOpen
é definido comotrue
, o componenteSearchBox
será renderizado na tela.
E renderize o componente de busca:
{isOpen && <SearchBox />}
Para mostrar o componente de busca, adicione uma função de alternância ao botão de entrada:
<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>
O evento onClick
define isOpen
como true
, exibindo o SearchBox
.
Mas como você viu, isso foi acionado por uma ação de clique, não por um atalho de teclado. Vamos fazer isso a seguir.
Como Acionar o Componente via Atalho de Teclado
Para fazer o componente de revelação abrir e fechar usando um atalho de teclado, usaremos um hook useEffect
para escutar combinações de teclas específicas e atualizar o estado do componente de acordo.
Passo 1: Escutar Eventos de Teclado
Adicione um hook useEffect
no seu arquivo App.tsx
para escutar pressionamentos de tecla:
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.ctrlKey && event.key === Key.K) {
event.preventDefault(); // Prevenir comportamento padrão do navegador
} };
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, []);
O que está acontecendo neste código?
-
Configuração do Efeito (
useEffect
)useEffect
garante que o ouvinte de eventos para pressionamentos de tecla seja adicionado quando o componente monta e limpo quando o componente desmonta, prevenindo vazamentos de memória.
-
Combinação de Teclas (
event.ctrlKey && event.key === "k"
)-
O
event.ctrlKey
verifica se a tecla Control está sendo pressionada. -
O
event.key === "k"
garante que estamos ouvindo especificamente a tecla “K”. Juntas, isso verifica se a combinação Ctrl + K foi pressionada.
-
-
Prevenir Comportamento Padrão (
event.preventDefault()
)- Alguns navegadores podem ter comportamentos padrão associados a combinações de teclas como Ctrl + K (por exemplo, focar na barra de endereços do navegador). Chamar
preventDefault
interrompe esse comportamento.
- Alguns navegadores podem ter comportamentos padrão associados a combinações de teclas como Ctrl + K (por exemplo, focar na barra de endereços do navegador). Chamar
-
Limpeza de Evento (
return () => ...
)- A função de limpeza remove o ouvinte de evento para evitar que ouvintes duplicados sejam adicionados se o componente for re-renderizado.
Passo 2: Alternar Visibilidade do Componente
Em seguida, atualize a função handleKeyDown
para alternar a visibilidade do SearchBox
quando o atalho for pressionado:
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// Ouvir Ctrl + K
if (event.ctrlKey && event.key === Key.K) {
event.preventDefault(); // Prevenir comportamento padrão do navegador
setIsOpen((prev) => !prev); // Alternar a caixa de pesquisa
} else if (event.key === Key.Escape) {
setIsOpen(false); // Fechar a caixa de pesquisa
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, []);
O que está acontecendo neste código?
-
Alternando Estado (
setIsOpen((prev) => !prev)
)-
Quando Ctrl + K é pressionado, o definidor de estado
setIsOpen
alterna a visibilidade daSearchBox
. -
O argumento
prev
representa o estado anterior. Usando!prev
inverte seu valor:-
true
(aberto) se tornafalse
(fechado). -
false
(fechado) se tornatrue
(aberto).
-
-
-
Fechando com a Tecla Escape (
event.key === "Escape"
)- Quando a tecla Escape é pressionada,
setIsOpen(false)
define explicitamente o estado comofalse
, fechando oSearchBox
.
- Quando a tecla Escape é pressionada,
Isso resulta no seguinte:
Como Animar a Visibilidade do Componente
No momento, nosso componente funciona, mas falta um pouco de estilo, não acha? Vamos mudar isso.
Passo 1: Criar o Componente Overlay
Vamos começar criando um componente overlay, que atua como o fundo escuro e desfocado para a caixa de pesquisa. Aqui está a versão base:
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>
);
}
Passo 2: Adicionar Animações ao Overlay
Agora, vamos fazer o overlay aparecer e desaparecer usando o Framer Motion. Atualize o componente OverlayWrapper
assim:
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>
);
}
Principais propriedades de animação:
-
initial
: Define o estado inicial quando o componente é montado (totalmente transparente). -
animate
: Define o estado para o qual animar (totalmente opaco). -
sair
: Especifica a animação quando o componente é desmontado (desvanecendo).
Passo 3: Animação da Caixa de Pesquisa
Em seguida, adicione um pouco de movimento à própria caixa de pesquisa. Faremos com que ela deslize e apareça ao desvanecer e deslize para fora ao desaparecer.
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>
);
}
Passo 4: Habilitar Rastreamento de Animação com AnimatePresence
Por fim, envolva sua lógica de renderização condicional no componente AnimatePresence
fornecido pelo Framer Motion. Isso garante que o Framer Motion rastreie quando os elementos entram e saem do DOM.
<AnimatePresence>{isOpen && <SearchBox />}</AnimatePresence>
Isso permite que o Framer Motion rastreie quando um elemento entra e sai do DOM. Com isso, obtemos o seguinte resultado:
Ah, muito melhor!
Como Otimizar Seu Componente KSL
Se você achou que tínhamos terminado, não tão rápido… Ainda temos um pouco mais a fazer.
Precisamos otimizar para acessibilidade. Devemos adicionar uma maneira para os usuários fecharem o componente de pesquisa com o mouse, pois a acessibilidade é muito importante.
Para fazer isso, comece criando um hook chamado useClickOutside
. Esse hook usa um elemento de referência para saber quando um usuário está clicando fora do elemento alvo (caixa de pesquisa), o que é um comportamento muito popular para fechar modais e KSLCs.
import { useEffect } from "react";
type ClickOutsideHandler = (event: Event) => void;
export const useClickOutside = (
ref: React.RefObject<HTMLElement>,
handler: ClickOutsideHandler
) => {
useEffect(() => {
const listener = (event: Event) => {
// Não fazer nada se estiver clicando no elemento de referência ou em elementos descendentes
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]);
};
Para usar esse hook, passe a função responsável por abrir e fechar o componente de pesquisa:
<AnimatePresence> {isOpen && <SearchBox close={setIsOpen} />} </AnimatePresence>
Então receba a função na busca com seu tipo de propriedade apropriado:
export default function SearchBox({
close,
}: {
close: React.Dispatch<React.SetStateAction<boolean>>;
}) {
Depois disso, crie uma referência (ref) para o item que você deseja rastrear e marque esse elemento:
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>
);
}
Em seguida, passe essa ref e a função a ser chamada quando um clique fora desse elemento for detectado.
useClickOutside(searchboxRef, () => close(false));
Testá-lo agora dá o seguinte resultado:
Também podemos otimizar o código um pouco mais. Como fizemos com o recurso de acessibilidade, podemos tornar nosso listener para detectar atalhos muito mais limpo e eficiente com os seguintes passos.
Primeiro, crie um arquivo de hook useKeyBindings
para lidar com combinações de teclas pressionadas.
Em seguida, defina o hook e a Interface. O hook aceitará um array de bindings, onde cada binding consiste em:
-
Um array de
keys
, que especifica a combinação de teclas (por exemplo, [“Control”, “k”]) -
Uma função de callback, que é chamada quando as teclas correspondentes são pressionadas.
import { useEffect } from "react";
// Defina a estrutura de um keybinding
interface KeyBinding {
keys: string[]; // Array de teclas (por exemplo, ["Control", "k"])
callback: () => void; // Função a ser executada quando as teclas são pressionadas
}
export const useKeyBindings = (bindings: KeyBinding[]) => {
};
Em seguida, crie a função handleKeyDown
. Dentro do hook, defina uma função que irá escutar eventos de teclado. Essa função verificará se as teclas pressionadas correspondem a alguma combinação de teclas definida.
Normalizaremos as teclas para minúsculas para que a comparação não seja sensível a maiúsculas e minúsculas e rastrearemos quais teclas estão pressionadas verificando ctrlKey
, shiftKey
, altKey
, metaKey
e a tecla pressionada (por exemplo, “k” para Ctrl + K).
const handleKeyDown = (event: KeyboardEvent) => {
// Rastrear as teclas que estão pressionadas
const pressedKeys = new Set<string>();
// Verificar teclas modificadoras (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");
// Adicionar a tecla que foi pressionada (por exemplo, "k" para Ctrl + K)
if (event.key) pressedKeys.add(event.key.toLowerCase());
};
Em seguida, compararemos as teclas pressionadas com o array de teclas das nossas ligações para verificar se elas correspondem. Se corresponderem, chamaremos a função de callback associada. Também garantimos que o número de teclas pressionadas corresponda ao número de teclas definidas na ligação.
// Loop através de cada ligação de tecla
bindings.forEach(({ keys, callback }) => {
// Normalizar as teclas para minúsculas para comparação
const normalizedKeys = keys.map((key) => key.toLowerCase());
// Verificar se as teclas pressionadas correspondem à ligação de tecla
const isMatch =
pressedKeys.size === normalizedKeys.length &&
normalizedKeys.every((key) => pressedKeys.has(key));
// Se as teclas corresponderem, chamar o callback
if (isMatch) {
event.preventDefault(); // Prevenir o comportamento padrão do navegador
callback(); // Executar a função de callback
}
});
Por fim, configure ouvintes de eventos no objeto window para escutar eventos de keydown. Esses ouvintes irão acionar a função handleKeyDown
sempre que uma tecla for pressionada. Certifique-se de limpar os ouvintes de eventos quando o componente for desmontado.
useEffect(() => {
// Adicione ouvintes de eventos para keydown
window.addEventListener("keydown", handleKeyDown);
// Limpeza dos ouvintes de eventos quando o componente for desmontado
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [bindings]);
O hook completo useKeyBindings
agora montado fica assim:
import { useEffect } from "react";
interface KeyBinding {
keys: string[]; // Uma combinação de teclas para acionar o callback (por exemplo, ["Control", "k"])
callback: () => void; // A função a ser executada quando as teclas forem pressionadas
}
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>();
// Rastrear teclas modificadoras explicitamente
if (event.ctrlKey) pressedKeys.add("control");
if (event.shiftKey) pressedKeys.add("shift");
if (event.altKey) pressedKeys.add("alt");
if (event.metaKey) pressedKeys.add("meta");
// Adicione a tecla realmente pressionada
if (event.key) pressedKeys.add(event.key.toLowerCase());
// Combinar exatamente: as teclas pressionadas devem corresponder às teclas definidas
const isExactMatch =
pressedKeys.size === normalizedKeys.length &&
normalizedKeys.every((key) => pressedKeys.has(key));
if (isExactMatch) {
event.preventDefault(); // Impedir o comportamento padrão
callback(); // Execute o callback
}
});
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [bindings]);
}
Veja como você pode usar este hook no seu App
:
import { useKeyBindings } from "./hooks/useKeyBindings";
export default function App() {
const [isOpen, setIsOpen] = useState<boolean>(false);
useKeyBindings([
{
keys: ["Control", "k"], // Escute por "Ctrl + K"
callback: () => setIsOpen((prev) => !prev), // Alternar a caixa de busca
},
{
keys: ["Escape"], // Escute por "Escape"
callback: () => setIsOpen(false), // Fechar a caixa de busca
},
]);
O que resulta no seguinte:
Com essa abordagem, você pode até adicionar vários atalhos para acionar a visibilidade do componente de busca.
useKeyBindings([
{
keys: ["Control", "k"], // Escute "Ctrl + K"
callback: () => setIsOpen((prev) => !prev), // Alternar a caixa de busca
},
{
keys: ["Control", "d"], // Escute "Ctrl + D"
callback: () => setIsOpen((prev) => !prev), // Alternar a caixa de busca
},
{
keys: ["Escape"], // Escute "Escape"
callback: () => setIsOpen(false), // Fechar a caixa de busca
},
]);
Aqui estão os links para todos os recursos que você pode precisar para este artigo:
Conclusão
Espero que este artigo tenha parecido um atalho bem cronometrado, levando você ao cerne da construção de componentes de atalho de teclado reutilizáveis. Com cada pressionamento de tecla e animação, você pode transformar experiências web comuns em extraordinárias.
Espero que seus atalhos ajudem você a criar aplicativos que se conectem com seus usuários. Afinal, as melhores jornadas frequentemente começam com a combinação certa.
Gosta dos meus artigos?
Sinta-se à vontade para me comprar um café aqui, para manter minha mente funcionando e fornecer mais artigos como este.
Informações de Contato
Quer se conectar ou me contatar? Sinta-se à vontade para me chamar nos seguintes:
-
Twitter / X: @jajadavid8
-
LinkedIn: David Jaja
-
Email: [email protected]