From @KVM's comment on @NVRM's answer, their approach returns the hash in Base64 format, instead of HEX.
Moreover, they use a somehow questionable approach for converting a String to an ArrayBuffer (which may have limitations).
A better approach would be with the native Web API TextEncoder. I've created an alternative method that uses TextEncoder and returns the value in hex instead Base64.
async function hmac(secretKey, message, algorithm = "SHA-256") {
    // Convert the message and secretKey to Uint8Array
    const encoder = new TextEncoder();
    const messageUint8Array = encoder.encode(message);
    const keyUint8Array = encoder.encode(secretKey);
    // Import the secretKey as a CryptoKey
    const cryptoKey = await window.crypto.subtle.importKey(
        "raw",
        keyUint8Array,
        { name: "HMAC", hash: algorithm },
        false,
        ["sign"]
    );
    // Sign the message with HMAC and the CryptoKey
    const signature = await window.crypto.subtle.sign(
        "HMAC",
        cryptoKey,
        messageUint8Array
    );
    // Convert the signature ArrayBuffer to a hex string
    const hashArray = Array.from(new Uint8Array(signature));
    const hashHex = hashArray
        .map((b) => b.toString(16).padStart(2, "0"))
        .join("");
    return hashHex;
}
// Example
const mySecretKey = "b";
const myMessage = "a";
hmac(mySecretKey, myMessage).then(h=>console.log(h)); // cb448b440c42ac8ad084fc8a8795c98f5b7802359c305eabd57ecdb20e248896