Glowing Keyboard

Keyboard which glows on hover

Basic
esc
F1
F2
F3
F4
F5
F6
F7
F8
F9
F10
F11
F12
~
`
!
1
@
2
#
3
$
4
%
5
^
6
&
7
*
8
(
9
)
0
_
-
+
=
delete
tab
Q
W
E
R
T
Y
U
I
O
P
{
[
}
]
|
\
caps lock
A
S
D
F
G
H
J
K
L
:
;
"
'
return
shift
Z
X
C
V
B
N
M
<
,
>
.
?
/
shift
fn
control
option
command
command
option
1

Install Dependencies

Install the dependencies for the CPU Architecture component.

bash
npm i clsx tailwind-merge lucide-react motion
2

Add Util File

Add a lib/utils.ts in your root directory.

utils.ts
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
3

Add CSS Classes

Add css classes in globals.css file.

globals.css
.glowing-key {
  box-shadow: 0px 0px 15px transparent;
  border-color: currentColor !important;
  animation: animate-key-border-shadow 1000ms linear forwards;
}

@keyframes animate-key-border-shadow {
  0% {
    box-shadow: 0px 0px 15px transparent;
  }
  100% {
    box-shadow: 0px 0px 20px currentColor;
  }
}

.blur-in {
  animation: blur-in 400ms ease-in-out forwards;
}

@keyframes blur-in {
  0% {
    filter: blur(1px);
  }
  100% {
    filter: blur(0px);
  }
}
4

Finally, Add SVG Component

Add a components/svg-components/glowing-keyboard.tsxcomponent in your components directory.

glowing-keyboard.tsx

"use client";

import { cn } from "@/lib/utils";
import {
  ArrowDownIcon,
  ArrowLeftIcon,
  ArrowRightIcon,
  ArrowUpIcon,
  ChevronUpIcon,
  CommandIcon,
  DotIcon,
  GlobeIcon,
  LayoutPanelLeft,
  MicIcon,
  MoonIcon,
  OptionIcon,
  PauseIcon,
  SearchIcon,
  SkipBackIcon,
  SkipForwardIcon,
  SunDimIcon,
  SunIcon,
  Volume1Icon,
  Volume2Icon,
  VolumeIcon,
} from "lucide-react";
import React, { useState, useCallback, memo } from "react";
import { motion } from "motion/react";

const iconSize = 12;

// Extract types
type CustomKeyType = "FingerPrintKey" | "ArrowKeys";

interface IconKeyboardKeyProps {
  icon: string | React.ReactNode;
  text?: string | React.ReactNode;
  className?: string;
  isSingleKey?: boolean;
  custom?: CustomKeyType;
  index?: number;
  rowIndex?: number;
  isHovered?: boolean;
  isLastRow?: boolean;
  keySize?: number;
  transparentKey?: boolean;
  glowColor?: string;
  highlight: {
    startRow: number;
    startIndex: number;
    text: string[];
  }[];
}

interface GlowingKeyboardSvgProps {
  isAlwaysActive?: boolean;
  transparentKey?: boolean;
  keySize?: number;
  glowColor?: string;
  highlight: {
    startRow: number;
    startIndex: number;
    text: string[];
  }[];
}

type KeyRowDataProps = Omit<IconKeyboardKeyProps, "highlight">;

const GlowingKeyboard = memo(
  ({
    isAlwaysActive = false,
    transparentKey = true,
    keySize = 40,
    glowColor = "#f43f5d",
    highlight,
  }: GlowingKeyboardSvgProps) => {
    const [isHovered, setIsHovered] = useState(false);

    const handleMouseEnter = useCallback(() => setIsHovered(true), []);
    const handleMouseLeave = useCallback(() => setIsHovered(false), []);

    return (
      <div
        onMouseEnter={handleMouseEnter}
        onMouseLeave={handleMouseLeave}
        className="card-color flex w-[700px] flex-col gap-2 rounded-md p-2"
      >
        {keysData.map((row, rowIndex) => {
          return (
            <div key={rowIndex} className="flex flex-row gap-2">
              {row.map((item, index) => {
                if (item.custom === "FingerPrintKey") {
                  return (
                    <FingerPrintKey
                      key={index}
                      index={index}
                      rowIndex={rowIndex}
                      keySize={keySize}
                      transparentKey={transparentKey}
                    />
                  );
                }

                if (item.custom === "ArrowKeys") {
                  return (
                    <ArrowKeys
                      key={index}
                      index={index}
                      rowIndex={rowIndex}
                      highlight={highlight}
                    />
                  );
                }

                return (
                  <IconKeyboardKey
                    key={index}
                    index={index}
                    rowIndex={rowIndex}
                    text={item.text}
                    icon={item.icon}
                    className={item.className}
                    isSingleKey={item.isSingleKey}
                    isHovered={isAlwaysActive ? true : isHovered}
                    isLastRow={rowIndex === keysData.length - 1}
                    keySize={keySize}
                    transparentKey={transparentKey}
                    glowColor={glowColor}
                    highlight={highlight}
                  />
                );
              })}
            </div>
          );
        })}
      </div>
    );
  }
);

GlowingKeyboard.displayName = "GlowingKeyboard";

const IconKeyboardKey = memo(
  ({
    icon,
    text,
    className,
    isSingleKey,
    index = 0,
    rowIndex = 0,
    isHovered,
    isLastRow,
    keySize,
    transparentKey,
    glowColor,
    highlight,
  }: IconKeyboardKeyProps) => {
    const data = getKeyData(highlight, index, text, isHovered, rowIndex);

    return (
      <motion.div
        variants={{
          initial: { borderColor: "#212121" },
          animate: {
            borderColor: ["#212121", "#383838", "#212121"],
          },
        }}
        initial="initial"
        animate="animate"
        transition={{
          duration: 0.8,
          ease: "easeInOut",
          delay: index * 0.1 + rowIndex * 0.12,
        }}
        style={{
          width: `${keySize}px`,
          height: `${keySize}px`,
          minWidth: `${keySize}px`,
          flexBasis: `${keySize}px`,
          color: data.shouldGlow ? `${glowColor}50` : "#212121",
        }}
        className={cn(
          className,
          data.shouldGlow
            ? "animate-[6000ms] glowing-key"
            : "hover:!border-white/20",
          "shadow-[inset_0px_0px_15px_rgba(0,0,0,0.2)] group relative flex select-none flex-col items-center justify-center gap-1.5 rounded-md border border-x-[1px] border-b-[0.1px] border-t-[1.5px] border-border/80 px-2 duration-200 first:items-start last:items-end active:!scale-[0.98]",
          transparentKey ? "bg-transparent" : "bg-[#121214]"
        )}
      >
        {!isSingleKey && (
          <div className="mt-0.5 h-[10px] w-[10px] text-center text-xs font-light text-muted-foreground/70 duration-200 group-hover:text-muted-foreground">
            {icon}
          </div>
        )}
        <div
          style={{
            color: data.shouldGlow ? glowColor : "",
          }}
          className={cn(
            isLastRow && "!text-[8px]",
            isSingleKey ? "text-sm" : "text-xs",
            data.shouldGlow &&
              "animate-[6000ms] drop-shadow-[2px_2px_8px_#f43f5d] blur-in",
            "text-center text-muted-foreground/70 duration-200 group-hover:text-white/60"
          )}
        >
          {data.text}
        </div>
      </motion.div>
    );
  }
);

IconKeyboardKey.displayName = "IconKeyboardKey";

const getKeyData = (
  highlight: {
    startRow: number;
    startIndex: number;
    text: string[];
  }[],
  index: number,
  text?: string | null | React.ReactNode,
  isHovered?: boolean,
  rowIndex?: number
) => {
  const isText = typeof text === "string";
  const modifiedKeysData = {
    text,
    shouldGlow: false,
  };

  for (const element of highlight) {
    if (element.text[index - element.startIndex] === "") {
      continue;
    }

    if (
      isText &&
      isHovered &&
      rowIndex === element.startRow &&
      index >= element.startIndex &&
      index <= element.startIndex + element.text.length - 1
    ) {
      modifiedKeysData.shouldGlow = true;
      modifiedKeysData.text = element.text[index - element.startIndex];
      break;
    }
  }

  return modifiedKeysData;
};

const FingerPrintKey = memo(
  ({
    index,
    rowIndex,
    keySize,
    transparentKey,
  }: {
    index: number;
    rowIndex: number;
    keySize: number;
    transparentKey: boolean;
  }) => {
    return (
      <motion.div
        variants={{
          initial: { borderColor: "#212121" },
          animate: {
            borderColor: ["#212121", "#383838", "#212121"],
          },
        }}
        initial="initial"
        animate="animate"
        transition={{
          duration: 0.6,
          ease: "easeInOut",
          delay: index * 0.1 + rowIndex * 0.12,
        }}
        style={{
          width: `${keySize}px`,
          height: `${keySize}px`,
          minWidth: `${keySize}px`,
        }}
        className={cn(
          "shadow-[inset_0px_0px_15px_rgba(0,0,0,0.2)] group relative flex flex-col items-center justify-center gap-1.5 rounded-md border border-x-[1px] border-b-[0.1px] border-t-[1.5px] border-border/80 duration-200 hover:border-white/20",
          transparentKey ? "bg-transparent" : "bg-[#121214]"
        )}
      >
        <div className="shadow-[inset_0px_0px_2px_rgba(0,0,0,0.6)] h-8 w-8 rounded-full bg-muted/20 duration-200 group-hover:bg-muted/35" />
      </motion.div>
    );
  }
);

FingerPrintKey.displayName = "FingerPrintKey";

const ArrowKeys = memo(
  ({
    index,
    rowIndex,
    highlight,
  }: {
    index: number;
    rowIndex: number;
    highlight: {
      startRow: number;
      startIndex: number;
      text: string[];
    }[];
  }) => {
    return (
      <div className="flex flex-row gap-2">
        <IconKeyboardKey
          index={index}
          rowIndex={rowIndex}
          isSingleKey
          className="!h-1/2 translate-y-5 !items-center"
          icon={null}
          text={<ArrowLeftIcon size={iconSize} />}
          highlight={highlight}
        />
        <div className="flex h-10 flex-col">
          <IconKeyboardKey
            isSingleKey
            index={index}
            rowIndex={rowIndex}
            className="!h-1/2 !items-center !rounded-b-none"
            icon={null}
            text={<ArrowUpIcon size={iconSize} />}
            highlight={highlight}
          />
          <IconKeyboardKey
            isSingleKey
            index={index}
            rowIndex={rowIndex}
            className="!h-1/2 !items-center !rounded-t-none"
            icon={null}
            text={<ArrowDownIcon size={iconSize} />}
            highlight={highlight}
          />
        </div>
        <IconKeyboardKey
          isSingleKey
          index={index}
          rowIndex={rowIndex}
          className="!h-1/2 translate-y-5 !items-center"
          icon={null}
          text={<ArrowRightIcon size={iconSize} />}
          highlight={highlight}
        />
      </div>
    );
  }
);

ArrowKeys.displayName = "ArrowKeys";

export default GlowingKeyboard;

const firstRowData: KeyRowDataProps[] = [
  {
    icon: null,
    text: "esc",
    className: "!basis-full",
  },
  {
    icon: <SunDimIcon size={iconSize} />,
    text: "F1",
  },
  {
    icon: <SunIcon size={iconSize} />,
    text: "F2",
  },
  {
    icon: <LayoutPanelLeft size={iconSize} />,
    text: "F3",
  },
  {
    icon: <SearchIcon size={iconSize} />,
    text: "F4",
  },
  {
    icon: <MicIcon size={iconSize} />,
    text: "F5",
  },
  {
    icon: <MoonIcon size={iconSize} />,
    text: "F6",
  },
  {
    icon: <SkipBackIcon size={iconSize} />,
    text: "F7",
  },
  {
    icon: <PauseIcon size={iconSize} />,
    text: "F8",
  },
  {
    icon: <SkipForwardIcon size={iconSize} />,
    text: "F9",
  },
  {
    icon: <VolumeIcon size={iconSize} />,
    text: "F10",
  },
  {
    icon: <Volume1Icon size={iconSize} />,
    text: "F11",
  },
  {
    icon: <Volume2Icon size={iconSize} />,
    text: "F12",
  },
  {
    custom: "FingerPrintKey",
    icon: null,
  },
];

const secondRowData: KeyRowDataProps[] = [
  {
    icon: "~",
    text: "`",
    className: "first:!items-center",
  },
  {
    icon: "!",
    text: "1",
  },
  {
    icon: "@",
    text: "2",
  },
  {
    icon: "#",
    text: "3",
  },
  {
    icon: "$",
    text: "4",
  },
  {
    icon: "%",
    text: "5",
  },
  {
    icon: "^",
    text: "6",
  },
  {
    icon: "&",
    text: "7",
  },
  {
    icon: "*",
    text: "8",
  },
  {
    icon: "(",
    text: "9",
  },
  {
    icon: ")",
    text: "0",
  },
  {
    icon: "_",
    text: "-",
  },
  {
    icon: "+",
    text: "=",
  },
  {
    icon: null,
    text: "delete",
    className: "!basis-full",
  },
];

const thirdRowData: KeyRowDataProps[] = [
  {
    icon: null,
    text: "tab",
    className: "!basis-full",
  },
  {
    icon: "q",
    text: "Q",
    isSingleKey: true,
  },
  {
    icon: "w",
    text: "W",
    isSingleKey: true,
  },
  {
    icon: "e",
    text: "E",
    isSingleKey: true,
  },
  {
    icon: "r",
    text: "R",
    isSingleKey: true,
  },
  {
    icon: "t",
    text: "T",
    isSingleKey: true,
  },
  {
    icon: "y",
    text: "Y",
    isSingleKey: true,
  },
  {
    icon: "u",
    text: "U",
    isSingleKey: true,
  },
  {
    icon: "i",
    text: "I",
    isSingleKey: true,
  },
  {
    icon: "o",
    text: "O",
    isSingleKey: true,
  },
  {
    icon: "p",
    text: "P",
    isSingleKey: true,
  },
  {
    icon: "{",
    text: "[",
  },
  {
    icon: "}",
    text: "]",
  },
  {
    icon: "|",
    text: "/",
    className: "last:!items-center",
  },
];

const fourthRowData: KeyRowDataProps[] = [
  {
    icon: <DotIcon size={iconSize + 3} className="-ml-1.5 -mt-0.5" />,
    text: "caps lock",
    className: "!basis-full",
  },
  {
    icon: "a",
    text: "A",
    isSingleKey: true,
  },
  {
    icon: "s",
    text: "S",
    isSingleKey: true,
  },
  {
    icon: "d",
    text: "D",
    isSingleKey: true,
  },
  {
    icon: "f",
    text: "F",
    isSingleKey: true,
  },
  {
    icon: "g",
    text: "G",
    isSingleKey: true,
  },
  {
    icon: "h",
    text: "H",
    isSingleKey: true,
  },
  {
    icon: "j",
    text: "J",
    isSingleKey: true,
  },
  {
    icon: "k",
    text: "K",
    isSingleKey: true,
  },
  {
    icon: "l",
    text: "L",
    isSingleKey: true,
  },
  {
    icon: ":",
    text: ";",
  },
  {
    icon: '"',
    text: "'",
  },
  {
    icon: null,
    text: "return",
    className: "!basis-full",
  },
];

const fifthRowData: KeyRowDataProps[] = [
  {
    icon: null,
    text: "shift",
    className: "!basis-full",
  },
  {
    icon: "z",
    text: "Z",
    isSingleKey: true,
  },
  {
    icon: "x",
    text: "X",
    isSingleKey: true,
  },
  {
    icon: "c",
    text: "C",
    isSingleKey: true,
  },
  {
    icon: "v",
    text: "V",
    isSingleKey: true,
  },
  {
    icon: "b",
    text: "B",
    isSingleKey: true,
  },
  {
    icon: "n",
    text: "N",
    isSingleKey: true,
  },
  {
    icon: "m",
    text: "M",
    isSingleKey: true,
  },
  {
    icon: "<",
    text: ",",
  },
  {
    icon: ">",
    text: ".",
  },
  {
    icon: "?",
    text: "/",
  },
  {
    icon: null,
    text: "shift",
    className: "!basis-full",
  },
];

const sixthRowData: KeyRowDataProps[] = [
  {
    icon: "fn",
    text: <GlobeIcon size={iconSize} />,
  },
  {
    icon: <ChevronUpIcon size={iconSize} />,
    text: "control",
    className: "!items-end",
  },
  {
    icon: <OptionIcon size={iconSize} />,
    text: "option",
    className: "!items-end",
  },
  {
    icon: <CommandIcon size={iconSize} />,
    text: "command",
    className: "!items-end !basis-20",
  },
  {
    icon: null,
    text: "",
    className: "!basis-80",
  },
  {
    icon: <CommandIcon size={iconSize} />,
    text: "command",
    className: "!items-start !basis-20",
  },
  {
    icon: <OptionIcon size={iconSize} />,
    text: "option",
    className: "!items-start",
  },
  {
    icon: null,
    custom: "ArrowKeys",
  },
];

const keysData = [
  firstRowData,
  secondRowData,
  thirdRowData,
  fourthRowData,
  fifthRowData,
  sixthRowData,
];

API Reference

The following table contains the API reference for the component.

PropTypeRequiredDefault
isAlwaysActivebooleanfalsefalse
transparentKeybooleanfalsetrue
keySizenumberfalse40
glowColorstringfalse#f43f5d
highlightobjecttrue-