Piano
Customizable piano component.
Source Style- React
- Svelte
- Vue
function App() { const synthRef = useRef<Tone.PolySynth | null>(null) useEffect(() => { const synth = new Tone.PolySynth({ volume: -6 }).toDestination() synthRef.current = synth return () => { synth.releaseAll() synth.dispose() if (synthRef.current === synth) synthRef.current = null } }, []) return ( <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', }} > <Piano.Root className={pianoTheme.root} classes={{ keyLabelWrapper: pianoTheme.keyLabelWrapper, keyLabel: pianoTheme.keyLabel, }} keyProps={(_note, { keyType }) => ({ className: keyType === 'white' ? pianoTheme.whiteKey : pianoTheme.blackKey, })} noteRange={{ first: noteNumber('C3'), last: noteNumber('B4') }} keyboardShortcuts={SHORTCUTS.HOME_ROW} onPlayNote={(noteNumber) => { synthRef.current?.triggerAttack(noteName(noteNumber)) }} onStopNote={(noteNumber) => { synthRef.current?.triggerRelease(noteName(noteNumber)) }} label={(_, { index }) => SHORTCUTS.HOME_ROW.keys[index]?.toUpperCase()} /> </div> ) }
/** The demo theme for `Piano`, as a CSS Module: one class per part, applied by* the caller. Copy this file into your project and edit it.*/.root {display: inline-block;box-sizing: border-box;user-select: none;touch-action: none;height: 160px;/* The width is the keyboard's own, so a resizable piano stretches both ways. */&[data-resizable] {height: 100%;}}.whiteKey {box-sizing: border-box;background-color: oklch(100% 0 0);-webkit-tap-highlight-color: transparent;color: oklch(0% 0 0);border: 1px solid #555;border-radius: 0px 0px 8px 8px;cursor: pointer;&[data-active] {background-color: oklch(92% 0 0);color: oklch(0% 0 0);}&[data-disabled] {cursor: not-allowed;}}.blackKey {box-sizing: border-box;background-color: oklch(32% 0 0);-webkit-tap-highlight-color: transparent;color: oklch(100% 0 0);border: 1px solid #555;border-radius: 0px 0px 8px 8px;cursor: pointer;&[data-active] {background-color: oklch(44% 0 0);color: oklch(100% 0 0);}&[data-disabled] {cursor: not-allowed;}}.keyLabelWrapper {display: flex;justify-content: center;align-items: end;height: 100%;}.keyLabel {box-sizing: content-box;display: flex;justify-content: center;align-items: center;font-size: 0.6rem;height: 10px;margin-bottom: 10px;padding: 4px;border-radius: 4px;border: 1px solid #888;aspect-ratio: 1;max-width: calc(100% - 16px);}/** Dark mode follows whatever marks the page — `.dark` and `[data-theme=dark]`* are what this site and Storybook set.*/:global(.dark) .whiteKey,:global([data-theme='dark']) .whiteKey {background-color: oklch(80% 0 0);color: oklch(0% 0 0);&[data-active] {background-color: oklch(72% 0 0);color: oklch(0% 0 0);}}
<script lang="ts">
import { SHORTCUTS } from '@tremolo-ui/dom'
import { noteName, noteNumber } from '@tremolo-ui/functions'
import { Piano } from '@tremolo-ui/svelte'
import * as Tone from 'tone'
// Copy this file from the Styling page into your own project.
import pianoTheme from './Piano.module.css'
let synth: Tone.PolySynth | null = null
$effect(() => {
const current = new Tone.PolySynth({ volume: -6 }).toDestination()
synth = current
return () => {
current.releaseAll()
current.dispose()
synth = null
}
})
</script>
<Piano.Root
class={pianoTheme.root}
classes={{
keyLabelWrapper: pianoTheme.keyLabelWrapper,
keyLabel: pianoTheme.keyLabel,
}}
keyProps={(_note, { keyType }) => ({
class: keyType === 'white' ? pianoTheme.whiteKey : pianoTheme.blackKey,
})}
noteRange={{ first: noteNumber('C3'), last: noteNumber('B4') }}
keyboardShortcuts={SHORTCUTS.HOME_ROW}
onPlayNote={(note) => synth?.triggerAttack(noteName(note))}
onStopNote={(note) => synth?.triggerRelease(noteName(note))}
>
{#snippet label(_note, { index })}
{SHORTCUTS.HOME_ROW.keys[index]?.toUpperCase()}
{/snippet}
</Piano.Root>
<script setup lang="ts">
import * as Tone from 'tone'
import { onBeforeUnmount, onMounted } from 'vue'
import { SHORTCUTS } from '@tremolo-ui/dom'
import { noteName, noteNumber } from '@tremolo-ui/functions'
import { Piano } from '@tremolo-ui/vue'
// Copy this file from the Styling page into your own project.
import pianoTheme from './Piano.module.css'
// Created once mounted: during server rendering there is no Web Audio.
let synth: Tone.PolySynth | null = null
onMounted(() => {
synth = new Tone.PolySynth({ volume: -6 }).toDestination()
})
onBeforeUnmount(() => {
synth?.releaseAll()
synth?.dispose()
synth = null
})
const play = (note: number) => synth?.triggerAttack(noteName(note))
const stop = (note: number) => synth?.triggerRelease(noteName(note))
</script>
<template>
<Piano
:class="pianoTheme.root"
:classes="{
keyLabelWrapper: pianoTheme.keyLabelWrapper,
keyLabel: pianoTheme.keyLabel,
}"
:key-props="
(_note, { keyType }) => ({
class: keyType === 'white' ? pianoTheme.whiteKey : pianoTheme.blackKey,
})
"
:note-range="{ first: noteNumber('C3'), last: noteNumber('B4') }"
:keyboard-shortcuts="SHORTCUTS.HOME_ROW"
@play-note="play"
@stop-note="stop"
>
<template #label="{ state }">
{{ SHORTCUTS.HOME_ROW.keys[state.index]?.toUpperCase() }}
</template>
</Piano>
</template>
Import
- React
- Svelte
- Vue
import { SHORTCUTS } from '@tremolo-ui/dom'
import { noteNumber, noteName } from '@tremolo-ui/functions'
import { Piano } from '@tremolo-ui/react'
import { SHORTCUTS } from '@tremolo-ui/dom'
import { noteNumber, noteName } from '@tremolo-ui/functions'
import { Piano } from '@tremolo-ui/svelte'
import { SHORTCUTS } from '@tremolo-ui/dom'
import { noteNumber, noteName } from '@tremolo-ui/functions'
import { Piano } from '@tremolo-ui/vue'
API Reference
Piano.Root also takes the attributes of the element it renders, such as
className and style.
In Svelte and Vue the props are the same, written their way — class rather
than className, bind:value or v-model, snippets or slots for content. See
Svelte and Vue.
State is on the element as a data attribute, so it is a selector away — see Styling. An attribute is only there while its state is on.
Root
Draws a key for every note in noteRange. It has no other parts: a key is
customised through label and keyProps.
| Prop | Type | Default |
|---|---|---|
classes | { keyLabelWrapper?: string; keyLabel?: string } | — |
noteRange* | NoteRange | — |
glissando | boolean | true |
midiMax | number | 127 |
keyboardShortcuts | KeyboardShortcuts | — |
keyboardShortcutsScope | KeyboardShortcutsScope | 'root' |
resizable | boolean | false |
whiteKeyWidth | number | 40 |
keyGap | number | 1 |
blackKeyWidthRatio | number | 0.65 |
blackKeyHeightRatio | number | 0.6 |
label | (note: number, state: KeyState) => ReactNode | — |
keyProps | (note: number, state: KeyState) => KeyAttributes | — |
onPlayNote | (note: number, velocity?: number) => void | — |
onStopNote | (note: number) => void | — |
actionsRef | Ref<PianoMethods> | — |
Full reference: PianoProps
The keyboard's height and each key's colours are set in CSS. Use keyProps to
give individual keys a class, style, or data-* attribute.
| Data attribute | Values |
|---|---|
[data-resizable] | Present when resizable is set |
Keys
Each key carries these.
| Data attribute | Values |
|---|---|
[data-note] | "0" … "127" |
[data-note-key] | "C" | "C#" | … | "B" |
[data-active] | Present while the note is sounding |
[data-disabled] | Present when the note is past midiMax |
A key takes more through keyProps, so a state of your own is one more
attribute:
keyProps={(note) => ({ 'data-in-scale': inScale(note, 0, 'major') ? '' : undefined })}