Interactive MIDI Keys in Framer

Copy component

Interactive MIDI Keys in Framer

Copy component

Nandi Muzsik

How can I improve Framer Uni?

Let me know if there’s a missing feature or something that could be improved.

Share feedback

Nandi Muzsik

How can I improve Framer Uni?

Let me know if there’s a missing feature or something that could be improved.

Share feedback

Component

Interactive MIDI Keys in Framer

This is an interactive keyboard created in Framer, based on the amazing design by Fons Mans. Feel free to copy the component or remix the project to dig deeper and play around with it.

image of Nandi Muzsik
profile image of Fons Mans

Created by

Interactive MIDI Keys in Framer
Interactive MIDI Keys in Framer
Interactive MIDI Keys in Framer

About the resource

The keys that make up the keyboard are SVG layers created in Figma and pasted back over to Framer. Usually, when you are creating detailed SVG illustrations with shadows and custom forms, it's better to use Figma and copy the SVGs over to Framer.

About the resource

The keys that make up the keyboard are SVG layers created in Figma and pasted back over to Framer. Usually, when you are creating detailed SVG illustrations with shadows and custom forms, it's better to use Figma and copy the SVGs over to Framer.

About the resource

The keys that make up the keyboard are SVG layers created in Figma and pasted back over to Framer. Usually, when you are creating detailed SVG illustrations with shadows and custom forms, it's better to use Figma and copy the SVGs over to Framer.

key designs in Figma

Key designs in Figma.

key designs in Figma

Key designs in Figma.

key designs in Figma

Key designs in Figma.

As you can see, each key has a "Default" and an "Active" design. In Framer, I created a component for each key with two variants: "Default" and "Active." Then, all I had to do was create an override that I can apply to each key so when they're clicked, they change to the "Active" variant and also play a note. You can find that override code below.

MIDI keyboard code override

Feel free to copy the code and create the override for yourself in your project by going to the assets panel, scrolling down to code, clicking the plus button, and creating a new code override file. Then, replace the default code with the code below.

As you can see, each key has a "Default" and an "Active" design. In Framer, I created a component for each key with two variants: "Default" and "Active." Then, all I had to do was create an override that I can apply to each key so when they're clicked, they change to the "Active" variant and also play a note. You can find that override code below.

MIDI keyboard code override

Feel free to copy the code and create the override for yourself in your project by going to the assets panel, scrolling down to code, clicking the plus button, and creating a new code override file. Then, replace the default code with the code below.

As you can see, each key has a "Default" and an "Active" design. In Framer, I created a component for each key with two variants: "Default" and "Active." Then, all I had to do was create an override that I can apply to each key so when they're clicked, they change to the "Active" variant and also play a note. You can find that override code below.

MIDI keyboard code override

Feel free to copy the code and create the override for yourself in your project by going to the assets panel, scrolling down to code, clicking the plus button, and creating a new code override file. Then, replace the default code with the code below.

import type { ComponentType } from "react"
import { useEffect, useRef, useCallback, useState } from "react"

const noteFrequencies = {
    C: 261.63 / 4,
    "C#": 277.18 / 4,
    D: 293.66 / 4,
    "D#": 311.13 / 4,
    E: 329.63 / 4,
    F: 349.23 / 4,
    "F#": 369.99 / 4,
    G: 392.0 / 4,
    "G#": 415.3 / 4,
    A: 440.0 / 4,
    "A#": 466.16 / 4,
    B: 493.88 / 4,
}

const noteKeys = {
    1: "d",
    2: "r",
    3: "f",
    4: "t",
    5: "g",
    6: "h",
    7: "u",
    8: "j",
    9: "i",
    10: "k",
    11: "o",
    12: "l",
}

function useWebAudio(noteFrequency: number) {
    const audioContextRef = useRef<AudioContext | null>(null)
    const oscillatorRef = useRef<OscillatorNode | null>(null)
    const gainNodeRef = useRef<GainNode | null>(null)

    useEffect(() => {
        if (!audioContextRef.current) {
            audioContextRef.current = new (window.AudioContext ||
                (window as any).webkitAudioContext)()
        }
    }, [])

    const playNote = useCallback(() => {
        if (!audioContextRef.current) return

        const audioCtx = audioContextRef.current

        // Stop the previous note if it's still playing
        if (oscillatorRef.current) {
            oscillatorRef.current.stop()
            gainNodeRef.current!.gain.exponentialRampToValueAtTime(
                0.001,
                audioCtx.currentTime + 0.03
            )
        }

        const oscillator = audioCtx.createOscillator()
        const gainNode = audioCtx.createGain()

        oscillator.type = "sawtooth" // Changed to 'sawtooth' for a synth-like sound
        oscillator.frequency.setValueAtTime(noteFrequency, audioCtx.currentTime)
        gainNode.gain.setValueAtTime(0.5, audioCtx.currentTime)

        oscillator.connect(gainNode)
        gainNode.connect(audioCtx.destination)

        oscillator.start()
        gainNode.gain.exponentialRampToValueAtTime(
            0.001,
            audioCtx.currentTime + 0.75
        )
        oscillator.stop(audioCtx.currentTime + 0.75)

        oscillatorRef.current = oscillator
        gainNodeRef.current = gainNode
    }, [noteFrequency])

    return playNote
}

function isTouchDevice() {
    if (typeof window === "undefined") return false
    return (
        "ontouchstart" in window ||
        navigator.maxTouchPoints > 0 ||
        navigator.msMaxTouchPoints > 0
    )
}

const createNoteOverride = (noteIndex: number) => {
    const noteKey = noteKeys[noteIndex]
    const noteFrequency = Object.values(noteFrequencies)[noteIndex - 1]

    return function (Component: ComponentType): ComponentType {
        return (props) => {
            const playNote = useWebAudio(noteFrequency)
            const [variant, setVariant] = useState("Default")
            const isPlaying = useRef(false)

            useEffect(() => {
                const handleKeyDown = (event: KeyboardEvent) => {
                    if (event.key === noteKey && !isPlaying.current) {
                        playNote()
                        setVariant("Active")
                        isPlaying.current = true
                    }
                }

                const handleKeyUp = (event: KeyboardEvent) => {
                    if (event.key === noteKey) {
                        setVariant("Default")
                        isPlaying.current = false
                    }
                }

                window.addEventListener("keydown", handleKeyDown)
                window.addEventListener("keyup", handleKeyUp)

                return () => {
                    window.removeEventListener("keydown", handleKeyDown)
                    window.removeEventListener("keyup", handleKeyUp)
                }
            }, [noteKey, playNote])

            const handleMouseDown = () => {
                if (!isPlaying.current) {
                    playNote()
                    setVariant("Active")
                    isPlaying.current = true
                }
                window.addEventListener("mouseup", handleMouseUp)
            }

            const handleMouseUp = () => {
                setVariant("Default")
                isPlaying.current = false
                window.removeEventListener("mouseup", handleMouseUp)
            }

            const handleTouchStart = () => {
                if (!isPlaying.current) {
                    playNote()
                    setVariant("Active")
                    isPlaying.current = true
                }
            }

            const handleTouchEnd = () => {
                setVariant("Default")
                isPlaying.current = false
            }

            const events = isTouchDevice()
                ? { onTouchStart: handleTouchStart, onTouchEnd: handleTouchEnd }
                : { onMouseDown: handleMouseDown, onMouseUp: handleMouseUp }

            return (
                <Component
                    {...props}
                    variant={variant}
                    {...events}
                    tabIndex="-1"
                    style={{
                        WebkitTapHighlightColor: "transparent",
                        outline: "none",
                    }}
                />
            )
        }
    }
}

// Generate note HOCs
export const withNote1 = (Component: ComponentType): ComponentType =>
    createNoteOverride(1)(Component)
export const withNote2 = (Component: ComponentType): ComponentType =>
    createNoteOverride(2)(Component)
export const withNote3 = (Component: ComponentType): ComponentType =>
    createNoteOverride(3)(Component)
export const withNote4 = (Component: ComponentType): ComponentType =>
    createNoteOverride(4)(Component)
export const withNote5 = (Component: ComponentType): ComponentType =>
    createNoteOverride(5)(Component)
export const withNote6 = (Component: ComponentType): ComponentType =>
    createNoteOverride(6)(Component)
export const withNote7 = (Component: ComponentType): ComponentType =>
    createNoteOverride(7)(Component)
export const withNote8 = (Component: ComponentType): ComponentType =>
    createNoteOverride(8)(Component)
export const withNote9 = (Component: ComponentType): ComponentType =>
    createNoteOverride(9)(Component)
export const withNote10 = (Component: ComponentType): ComponentType =>
    createNoteOverride(10)(Component)
export const withNote11 = (Component: ComponentType): ComponentType =>
    createNoteOverride(11)(Component)
export const withNote12 = (Component: ComponentType): ComponentType =>
    createNoteOverride(12)(Component)
import type { ComponentType } from "react"
import { useEffect, useRef, useCallback, useState } from "react"

const noteFrequencies = {
    C: 261.63 / 4,
    "C#": 277.18 / 4,
    D: 293.66 / 4,
    "D#": 311.13 / 4,
    E: 329.63 / 4,
    F: 349.23 / 4,
    "F#": 369.99 / 4,
    G: 392.0 / 4,
    "G#": 415.3 / 4,
    A: 440.0 / 4,
    "A#": 466.16 / 4,
    B: 493.88 / 4,
}

const noteKeys = {
    1: "d",
    2: "r",
    3: "f",
    4: "t",
    5: "g",
    6: "h",
    7: "u",
    8: "j",
    9: "i",
    10: "k",
    11: "o",
    12: "l",
}

function useWebAudio(noteFrequency: number) {
    const audioContextRef = useRef<AudioContext | null>(null)
    const oscillatorRef = useRef<OscillatorNode | null>(null)
    const gainNodeRef = useRef<GainNode | null>(null)

    useEffect(() => {
        if (!audioContextRef.current) {
            audioContextRef.current = new (window.AudioContext ||
                (window as any).webkitAudioContext)()
        }
    }, [])

    const playNote = useCallback(() => {
        if (!audioContextRef.current) return

        const audioCtx = audioContextRef.current

        // Stop the previous note if it's still playing
        if (oscillatorRef.current) {
            oscillatorRef.current.stop()
            gainNodeRef.current!.gain.exponentialRampToValueAtTime(
                0.001,
                audioCtx.currentTime + 0.03
            )
        }

        const oscillator = audioCtx.createOscillator()
        const gainNode = audioCtx.createGain()

        oscillator.type = "sawtooth" // Changed to 'sawtooth' for a synth-like sound
        oscillator.frequency.setValueAtTime(noteFrequency, audioCtx.currentTime)
        gainNode.gain.setValueAtTime(0.5, audioCtx.currentTime)

        oscillator.connect(gainNode)
        gainNode.connect(audioCtx.destination)

        oscillator.start()
        gainNode.gain.exponentialRampToValueAtTime(
            0.001,
            audioCtx.currentTime + 0.75
        )
        oscillator.stop(audioCtx.currentTime + 0.75)

        oscillatorRef.current = oscillator
        gainNodeRef.current = gainNode
    }, [noteFrequency])

    return playNote
}

function isTouchDevice() {
    if (typeof window === "undefined") return false
    return (
        "ontouchstart" in window ||
        navigator.maxTouchPoints > 0 ||
        navigator.msMaxTouchPoints > 0
    )
}

const createNoteOverride = (noteIndex: number) => {
    const noteKey = noteKeys[noteIndex]
    const noteFrequency = Object.values(noteFrequencies)[noteIndex - 1]

    return function (Component: ComponentType): ComponentType {
        return (props) => {
            const playNote = useWebAudio(noteFrequency)
            const [variant, setVariant] = useState("Default")
            const isPlaying = useRef(false)

            useEffect(() => {
                const handleKeyDown = (event: KeyboardEvent) => {
                    if (event.key === noteKey && !isPlaying.current) {
                        playNote()
                        setVariant("Active")
                        isPlaying.current = true
                    }
                }

                const handleKeyUp = (event: KeyboardEvent) => {
                    if (event.key === noteKey) {
                        setVariant("Default")
                        isPlaying.current = false
                    }
                }

                window.addEventListener("keydown", handleKeyDown)
                window.addEventListener("keyup", handleKeyUp)

                return () => {
                    window.removeEventListener("keydown", handleKeyDown)
                    window.removeEventListener("keyup", handleKeyUp)
                }
            }, [noteKey, playNote])

            const handleMouseDown = () => {
                if (!isPlaying.current) {
                    playNote()
                    setVariant("Active")
                    isPlaying.current = true
                }
                window.addEventListener("mouseup", handleMouseUp)
            }

            const handleMouseUp = () => {
                setVariant("Default")
                isPlaying.current = false
                window.removeEventListener("mouseup", handleMouseUp)
            }

            const handleTouchStart = () => {
                if (!isPlaying.current) {
                    playNote()
                    setVariant("Active")
                    isPlaying.current = true
                }
            }

            const handleTouchEnd = () => {
                setVariant("Default")
                isPlaying.current = false
            }

            const events = isTouchDevice()
                ? { onTouchStart: handleTouchStart, onTouchEnd: handleTouchEnd }
                : { onMouseDown: handleMouseDown, onMouseUp: handleMouseUp }

            return (
                <Component
                    {...props}
                    variant={variant}
                    {...events}
                    tabIndex="-1"
                    style={{
                        WebkitTapHighlightColor: "transparent",
                        outline: "none",
                    }}
                />
            )
        }
    }
}

// Generate note HOCs
export const withNote1 = (Component: ComponentType): ComponentType =>
    createNoteOverride(1)(Component)
export const withNote2 = (Component: ComponentType): ComponentType =>
    createNoteOverride(2)(Component)
export const withNote3 = (Component: ComponentType): ComponentType =>
    createNoteOverride(3)(Component)
export const withNote4 = (Component: ComponentType): ComponentType =>
    createNoteOverride(4)(Component)
export const withNote5 = (Component: ComponentType): ComponentType =>
    createNoteOverride(5)(Component)
export const withNote6 = (Component: ComponentType): ComponentType =>
    createNoteOverride(6)(Component)
export const withNote7 = (Component: ComponentType): ComponentType =>
    createNoteOverride(7)(Component)
export const withNote8 = (Component: ComponentType): ComponentType =>
    createNoteOverride(8)(Component)
export const withNote9 = (Component: ComponentType): ComponentType =>
    createNoteOverride(9)(Component)
export const withNote10 = (Component: ComponentType): ComponentType =>
    createNoteOverride(10)(Component)
export const withNote11 = (Component: ComponentType): ComponentType =>
    createNoteOverride(11)(Component)
export const withNote12 = (Component: ComponentType): ComponentType =>
    createNoteOverride(12)(Component)
import type { ComponentType } from "react"
import { useEffect, useRef, useCallback, useState } from "react"

const noteFrequencies = {
    C: 261.63 / 4,
    "C#": 277.18 / 4,
    D: 293.66 / 4,
    "D#": 311.13 / 4,
    E: 329.63 / 4,
    F: 349.23 / 4,
    "F#": 369.99 / 4,
    G: 392.0 / 4,
    "G#": 415.3 / 4,
    A: 440.0 / 4,
    "A#": 466.16 / 4,
    B: 493.88 / 4,
}

const noteKeys = {
    1: "d",
    2: "r",
    3: "f",
    4: "t",
    5: "g",
    6: "h",
    7: "u",
    8: "j",
    9: "i",
    10: "k",
    11: "o",
    12: "l",
}

function useWebAudio(noteFrequency: number) {
    const audioContextRef = useRef<AudioContext | null>(null)
    const oscillatorRef = useRef<OscillatorNode | null>(null)
    const gainNodeRef = useRef<GainNode | null>(null)

    useEffect(() => {
        if (!audioContextRef.current) {
            audioContextRef.current = new (window.AudioContext ||
                (window as any).webkitAudioContext)()
        }
    }, [])

    const playNote = useCallback(() => {
        if (!audioContextRef.current) return

        const audioCtx = audioContextRef.current

        // Stop the previous note if it's still playing
        if (oscillatorRef.current) {
            oscillatorRef.current.stop()
            gainNodeRef.current!.gain.exponentialRampToValueAtTime(
                0.001,
                audioCtx.currentTime + 0.03
            )
        }

        const oscillator = audioCtx.createOscillator()
        const gainNode = audioCtx.createGain()

        oscillator.type = "sawtooth" // Changed to 'sawtooth' for a synth-like sound
        oscillator.frequency.setValueAtTime(noteFrequency, audioCtx.currentTime)
        gainNode.gain.setValueAtTime(0.5, audioCtx.currentTime)

        oscillator.connect(gainNode)
        gainNode.connect(audioCtx.destination)

        oscillator.start()
        gainNode.gain.exponentialRampToValueAtTime(
            0.001,
            audioCtx.currentTime + 0.75
        )
        oscillator.stop(audioCtx.currentTime + 0.75)

        oscillatorRef.current = oscillator
        gainNodeRef.current = gainNode
    }, [noteFrequency])

    return playNote
}

function isTouchDevice() {
    if (typeof window === "undefined") return false
    return (
        "ontouchstart" in window ||
        navigator.maxTouchPoints > 0 ||
        navigator.msMaxTouchPoints > 0
    )
}

const createNoteOverride = (noteIndex: number) => {
    const noteKey = noteKeys[noteIndex]
    const noteFrequency = Object.values(noteFrequencies)[noteIndex - 1]

    return function (Component: ComponentType): ComponentType {
        return (props) => {
            const playNote = useWebAudio(noteFrequency)
            const [variant, setVariant] = useState("Default")
            const isPlaying = useRef(false)

            useEffect(() => {
                const handleKeyDown = (event: KeyboardEvent) => {
                    if (event.key === noteKey && !isPlaying.current) {
                        playNote()
                        setVariant("Active")
                        isPlaying.current = true
                    }
                }

                const handleKeyUp = (event: KeyboardEvent) => {
                    if (event.key === noteKey) {
                        setVariant("Default")
                        isPlaying.current = false
                    }
                }

                window.addEventListener("keydown", handleKeyDown)
                window.addEventListener("keyup", handleKeyUp)

                return () => {
                    window.removeEventListener("keydown", handleKeyDown)
                    window.removeEventListener("keyup", handleKeyUp)
                }
            }, [noteKey, playNote])

            const handleMouseDown = () => {
                if (!isPlaying.current) {
                    playNote()
                    setVariant("Active")
                    isPlaying.current = true
                }
                window.addEventListener("mouseup", handleMouseUp)
            }

            const handleMouseUp = () => {
                setVariant("Default")
                isPlaying.current = false
                window.removeEventListener("mouseup", handleMouseUp)
            }

            const handleTouchStart = () => {
                if (!isPlaying.current) {
                    playNote()
                    setVariant("Active")
                    isPlaying.current = true
                }
            }

            const handleTouchEnd = () => {
                setVariant("Default")
                isPlaying.current = false
            }

            const events = isTouchDevice()
                ? { onTouchStart: handleTouchStart, onTouchEnd: handleTouchEnd }
                : { onMouseDown: handleMouseDown, onMouseUp: handleMouseUp }

            return (
                <Component
                    {...props}
                    variant={variant}
                    {...events}
                    tabIndex="-1"
                    style={{
                        WebkitTapHighlightColor: "transparent",
                        outline: "none",
                    }}
                />
            )
        }
    }
}

// Generate note HOCs
export const withNote1 = (Component: ComponentType): ComponentType =>
    createNoteOverride(1)(Component)
export const withNote2 = (Component: ComponentType): ComponentType =>
    createNoteOverride(2)(Component)
export const withNote3 = (Component: ComponentType): ComponentType =>
    createNoteOverride(3)(Component)
export const withNote4 = (Component: ComponentType): ComponentType =>
    createNoteOverride(4)(Component)
export const withNote5 = (Component: ComponentType): ComponentType =>
    createNoteOverride(5)(Component)
export const withNote6 = (Component: ComponentType): ComponentType =>
    createNoteOverride(6)(Component)
export const withNote7 = (Component: ComponentType): ComponentType =>
    createNoteOverride(7)(Component)
export const withNote8 = (Component: ComponentType): ComponentType =>
    createNoteOverride(8)(Component)
export const withNote9 = (Component: ComponentType): ComponentType =>
    createNoteOverride(9)(Component)
export const withNote10 = (Component: ComponentType): ComponentType =>
    createNoteOverride(10)(Component)
export const withNote11 = (Component: ComponentType): ComponentType =>
    createNoteOverride(11)(Component)
export const withNote12 = (Component: ComponentType): ComponentType =>
    createNoteOverride(12)(Component)

Framer Navigator

Learn the fundamentals of Framer for free.

Build your ideas with ease by learning the basics of website building with Framer.

Nandi portrait's background
Nandi's portrait

Framer Navigator

Learn the fundamentals of Framer for free.

Build your ideas with ease by learning the basics of website building with Framer.

Nandi portrait's background
Nandi's portrait

Framer Navigator

Learn the fundamentals of Framer for free.

Build your ideas with ease by learning the basics of website building with Framer.

Nandi portrait's background
Nandi's portrait

More resources

More resources

  • Creative design studio banner featuring staggered typography and motion-inspired visuals

    Staggered Text Cycle Component for Framer

    Component

    Creative design studio banner featuring staggered typography and motion-inspired visuals

    Staggered Text Cycle Component for Framer

    Component

    Creative design studio banner featuring staggered typography and motion-inspired visuals

    Staggered Text Cycle Component for Framer

    Component

  • 3D slideshow showcasing modern cycling and lifestyle themes

    Smooth 3D Slideshow in Framer

    Component

    3D slideshow showcasing modern cycling and lifestyle themes

    Smooth 3D Slideshow in Framer

    Component

    3D slideshow showcasing modern cycling and lifestyle themes

    Smooth 3D Slideshow in Framer

    Component