Se você é como eu e adora atalhos, sabe como é 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 expert em tecnologia.
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.
Índice
Aqui está tudo o que vamos cobrir:
Pré-requisitos
-
Fundamentos de HTML, CSS e Tailwind CSS
-
Fundamentos de JavaScript, React e React Hooks.
O que é um Componente Listener de Atalho de Teclado (KSL)?
Um componente Listener de Atalhos de Teclado (KSLC) é um componente que fica atento a combinações de teclas específicas e aciona ações em seu aplicativo. Ele é projetado para fazer com que seu aplicativo responda a atalhos de teclado, permitindo uma experiência do usuário mais suave e eficiente.
Por que é importante?
-
Acessibilidade: O componente KSL torna simples para pessoas que usam um teclado acionar ações, tornando seu aplicativo mais inclusivo e fácil de usar.
-
Experiência mais ágil: Atalhos são rápidos e eficientes, permitindo que os usuários realizem tarefas em menos tempo. Não é mais preciso procurar o mouse por aí – 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 seu aplicativo, facilitando a adição 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 ao centralizar a lógica. Seu código permanece limpo, organizado e mais fácil de manter.
Como Criar o Componente KSL
Preparei um repositório no GitHub com arquivos iniciais para acelerar as coisas. Basta clonar este repositório e instalar as dependências.
Para este projeto, estamos usando a página inicial do Tailwind como nossa inspiração e criando a funcionalidade KSL. Após instalar e executar o comando de build, aqui está como sua página deve parecer:
Como Criar o Componente Reveal
O componente reveal é o componente que queremos mostrar quando usamos 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 fundo.
-
O
backdrop-blur-sm
adiciona um desfoque sutil ao fundo, ebg-slate-900/50
proporciona uma sobreposição escura semi-transparente.
-
-
Wrapper da Caixa de Pesquisa (
<div className="p-[15vh] ...">
)-
O conteúdo é centralizado usando padding e utilitários flex.
-
O
max-w-xl
garante que a caixa de pesquisa permaneça dentro de uma largura razoável para legibilidade.
-
Em seu App.tsx
, crie um estado que mostre esse componente dinamicamente:
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 uma ação de 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 o 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 é montado e limpo quando o componente é desmontado, 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”. Juntos, 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ço do navegador). Chamar
preventDefault
impede 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ço do navegador). Chamar
-
Limpeza de Evento (
return () => ...
)- A função de limpeza remove o ouvinte de evento para evitar que ouvintes duplicados sejam adicionados caso o componente seja renderizado novamente.
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 setter de estado
setIsOpen
alterna a visibilidade doSearchBox
. -
O argumento
prev
representa o estado anterior. Usando!prev
, seu valor é invertido:-
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, você não acha? Vamos mudar isso.
Passo 1: Criar o Componente de Sobreposição
Começaremos criando um componente de sobreposição, que atua como o fundo escuro e desfocado para a caixa de pesquisa. Aqui está a versão básica:
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 à Sobreposição
Agora, vamos fazer a sobreposição desvanecer para dentro e para fora 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>
);
}
Propriedades principais de animação:
-
initial
: Define o estado inicial quando o componente é montado (totalmente transparente). -
animate
: Define o estado para o qual animar (totalmente opaco). -
exit
: Especifica a animação quando o componente é desmontado (desaparecendo).
Etapa 3: Anime a Caixa de Pesquisa
Em seguida, adicione um pouco de movimento à própria caixa de pesquisa. Vamos fazer com que ela deslize e desapareça quando aparecer e deslize para fora quando 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>
);
}
Etapa 4: Ative o Rastreamento de Animações 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ê pensou que havíamos 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
. Este hook usa um elemento de referência para saber quando um usuário está clicando fora do elemento alvo (caixa de pesquisa), 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 faça nada se clicar no elemento da 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 este 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 o 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 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));
Testando agora, o resultado é o seguinte.
Também podemos otimizar um pouco mais o código. Assim como fizemos com o recurso de acessibilidade, podemos tornar nosso ouvinte de detecção de atalhos mais limpo e eficiente com os seguintes passos.
Primeiro, crie um arquivo de gancho useKeyBindings
para lidar com combinações de pressionamento de tecla.
Em seguida, defina o gancho e a Interface. O gancho aceitará um array de vinculações, onde cada vinculação consiste em:
-
Um array de
keys
, que especifica a combinação de teclas (por exemplo, [“Control”, “k”]) -
Uma função de retorno, que é chamada quando as teclas correspondentes são pressionadas.
import { useEffect } from "react";
// Definir a estrutura de uma vinculação de teclas
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.
Vamos normalizar as teclas para minúsculas para que a comparação não seja sensível a maiúsculas e minúsculas e rastrear 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 as 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, vamos comparar as teclas pressionadas com o array de teclas de nossos bindings para verificar se 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 no binding.
// Percorrer cada keybinding
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 ao keybinding
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
}
});
Finalmente, 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 são 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>();
// Rastreie as 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 pressionada real
if (event.key) pressedKeys.add(event.key.toLowerCase());
// Correspondência exata: 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 em 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 pesquisa
},
{
keys: ["Escape"], // Escute por "Escape"
callback: () => setIsOpen(false), // Fechar a caixa de pesquisa
},
]);
O que resulta no seguinte:
Com esta abordagem, você pode até adicionar vários atalhos para ativar a visibilidade do componente de busca.
useKeyBindings([
{
keys: ["Control", "k"], // Ouça por "Ctrl + K"
callback: () => setIsOpen((prev) => !prev), // Alternar a caixa de busca
},
{
keys: ["Control", "d"], // Ouça por "Ctrl + D"
callback: () => setIsOpen((prev) => !prev), // Alternar a caixa de busca
},
{
keys: ["Escape"], // Ouça por "Escape"
callback: () => setIsOpen(false), // Fechar a caixa de busca
},
]);
Aqui estão 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 reutilizáveis de atalhos de teclado. Com cada pressionamento de tecla e animação, você agora pode transformar experiências web comuns em experiências extraordinárias.
Espero que seus atalhos ajudem você a criar aplicativos que tenham sintonia com seus usuários. Afinal, as melhores jornadas muitas vezes começam com a combinação certa.
Gosta dos meus artigos?
Sinta-se à vontade para me comprar um café aqui, para manter meu cérebro funcionando e fornecer mais artigos como este.
Informações de Contato
Quer se conectar ou entrar em contato comigo? Sinta-se à vontade para me chamar nos seguintes:
-
Twitter / X: @jajadavid8
-
LinkedIn: David Jaja
-
Email: [email protected]