acs docs

ACS Documentation

ACS (Audio Cascading Style Sheets) is a CSS-like declarative stylesheet language for audio. Where CSS describes how an HTML element should look, ACS describes how it should sound. Same selectors, same cascade, same specificity rules — but properties target audio: sound, volume, pitch, room, sound-mood.

One <link rel="audiostyle">, one <script>, and your buttons sound like buttons. No event handlers, no play() calls. The cascade does the wiring.

Everything is declarative: rules live in .acs files, the runtime attaches one document-level listener per event type, and at fire time it walks up from the click target resolving the cascade at each ancestor — so dynamic state (added classes, attribute changes) is always current.

Install

Pick whichever distribution channel fits your stack — there is no build step required.

1 · Browser via CDN (zero install)

<link rel="audiostyle" href="my-style.acs" />
<script type="module"
        src="https://cdn.jsdelivr.net/gh/Grkmyldz148/acs@main/poc/runtime.js"></script>

jsDelivr auto-mirrors the GitHub repo. For pinned versions use @v0.9.2 instead of @main.

2 · npm

npm install acs-audio

Then in any entry file:

import "acs-audio";   // side-effect import — auto-binds <link rel="audiostyle">

The runtime is framework-agnostic: works with React, Vue, Svelte, plain HTML, etc. TypeScript types ship in the package.

3 · Self-host

Download dist/runtime.mjs from the GitHub Releases page and serve it next to your app. The runtime auto-loads defaults.acs (49 calibrated built-in presets) from the same directory, so keep it co-located.

4 · VSCode / Cursor / VSCodium extension

One-click install from your editor's marketplace:

Or via the editor's CLI:

code   --install-extension audio-cascading-style-sheets.acs-language   # VSCode
cursor --install-extension audio-cascading-style-sheets.acs-language   # Cursor
codium --install-extension audio-cascading-style-sheets.acs-language   # VSCodium

Provides syntax highlighting, outline, folding, hover docs, live linter with fuzzy-match hints (did you mean "master-volume"?), ▶ audition CodeLens, sound picker webview with knob-based layer editor, and 30+ snippets.

Your first stylesheet

Create my-style.acs next to your index.html:

:root            { master-volume: 0.85; room: medium-room; }

button           { sound-on-click: tap-tactile; }
button.primary   { sound-on-click: pop;     pitch: +1st; }
button.danger    { sound-on-click: error;   volume: 0.7; }
input:on-focus   { sound: tick; volume: 0.4; }
input:on-input   { sound: click-soft; }
[role=alert]     { sound-on-appear: notify; }
dialog[open]     { sound-on-appear: modal-open; room: small-room; }

@media (prefers-reduced-sound: reduce) {
  :root { master-volume: 0; }
}

Link it from your HTML:

<link rel="audiostyle" href="my-style.acs" />
<script type="module" src="https://cdn.jsdelivr.net/gh/Grkmyldz148/acs@main/poc/runtime.js"></script>

That's it. Click any button — the runtime resolved the cascade, looked up the preset, played a sound.

Autoplay note. Browsers block AudioContext until the user interacts. ACS auto-resumes on the first pointerdown / keydown. The first click of a session unlocks audio for everything that follows.

How it works

The runtime is a tiny browser-side parser plus a Web Audio engine:

.acs files
   → parse — brace-aware CSS-like parser, handles @media nesting
   → split: :root → master config, @sound → custom presets, rest → resolver
   → buildBindings — produces resolver(el) → { click[], enter[], focus[], input[] }
   ↓
ONE document-level listener per event type. At fire time:
   walk up from event.target,
   call resolver(ancestor) at each step (dynamic state always current),
   flatten matching rules,
   apply inheritance for inherited props,
   trigger preset with merged decls
   ↓
preset runner → voice graph (modal / tones / pluck / osc / noise)
   → optional saturation + pan
   → room chain (dry + convolver + wet)
   → master EQ → master gain → limiter → speakers

Calibration runs once at startup: every preset is offline-rendered, K-weighted RMS is measured, and a per-preset multiplier is baked so volume: 0.5 sounds equally loud whether the preset is a tap or a gong. Post-cal spread across all 49 built-in presets is 4.8 dB — well under the ITU-R BS.1770 broadcast tolerance.

Concepts

ACS is structured like CSS. If you can read a CSS file, you can read ACS. Below is the full mental model — each subsection is a concept, each concept is a single ACS file you could write end-to-end.

Properties

ACS properties are kebab-case, just like CSS. Three categories: per-event triggers, per-element modifiers, and root-level master config.

Per-event triggers

PropertyFires whenExample
sound-on-clickClick bubbles to this elementtap-tactile
sound-on-enterpointerenter (hover-style)tick
sound-on-focusfocusintick
sound-on-inputForm input eventclick-soft
sound-on-appearElement added to DOM (MutationObserver)notify
sound-on-leaveElement removed from DOMmodal-close
soundGeneric — used inside a state pseudo-class like :on-click, :on-inputpop

Per-element modifiers (inheritable)

PropertyDefaultRange / values
volume10..1 · or X !raw to bypass calibration
pitch0Semitones, e.g. +1st, -3st
roomdrydry · small-room · medium-room · large-room · hall · plate
sound-mood9 overlays (see Moods)
sound-mood-mix10..1 wet/dry blend
pan0-1 (left) → 1 (right)

Root-only (master)

PropertyEffect
master-volumeGlobal gain (post-limiter input)
master-eqTone tilt at master bus
qualitylow | medium | high — caps voice pool, modal partials, reverb tail

Cascade & selectors

Specificity is computed exactly like CSS: (inline=irrelevant, ids, classes/attrs/pseudo-classes, types/pseudo-elements). More specific wins; !important overrides; later wins on tie.

SelectorSpecificityExample
type0,0,1button
class0,1,0.primary
attribute0,1,0[data-variant="ghost"]
id1,0,0#cta
compoundsumbutton.primary[disabled] = 0,2,1
descendantsumdialog[open] button = 0,1,2
universal0,0,0*
pseudo-state0,1,0:on-click, :on-input

Inheritance: sound-mood, sound-mood-mix, volume, pitch, room, pan all inherit. Set sound-mood: lofi on body and the entire app gets a muffled overlay until you override on a child.

Layer sources

Every @sound is built from one or more layers. A layer is a single voice graph — pick one of these source primitives, configure it, the runtime renders it through saturation → pan → room → master.

Layer source types

Source keyMeaningBest for
modalModal IIR resonator (frequency + ratios + decays)Bells, gongs, struck objects
tonesAdditive sine bank — comma-separated frequenciesChords, bell-like clusters, UI dings
oscSingle oscillator (sine | triangle | square | sawtooth) with optional fmSynth tones, FM bells, sweeps
pluckKarplus-Strong plucked stringGuitar/harp-like accents
noiseFiltered noise (white | pink | brown)Snares, claps, breath, swooshes

Concrete examples

@sound my-bell {
  body {
    tones: 880hz, 1320hz, 2200hz;     /* additive */
    decays: 0.6s, 0.4s, 0.25s;
    gain: 0.4;
  }
}

@sound my-pluck {
  body { pluck: 440hz; brightness: 0.7; decay: 1.2s; gain: 0.5; }
}

@sound my-snare {
  body  { noise: white; filter: bandpass; cutoff: 2khz; q: 2; decay: 80ms; gain: 0.6; }
  snap  { osc: sine;    freq: 200hz;     decay: 30ms;  gain: 0.3; }
}

Multiple layers stack additively — both fire simultaneously and the runtime sums them.

@sound blocks

@sound <name> { ... } registers a custom preset by name. Once registered, any selector can use it via sound: my-name, sound-on-click: my-name, etc.

@sound bubble {
  body { osc: sine; freq: 540hz; pitch-from: 540hz to 880hz; decay: 80ms; gain: 0.5; }
  pop  { noise: white; filter: highpass; cutoff: 4khz; decay: 12ms; gain: 0.2; }
}

button.tooltip-trigger:on-hover { sound: bubble; pitch: +2st; }

Re-defining a preset name overrides the built-in. So @sound tap { ... } in your stylesheet replaces the calibrated tap. The 49 built-ins are loaded first, your stylesheet last — last wins.

@sample at-rule

Bring your own audio files. Body-less; registers a URL under a preset name. The file is fetched + cached on first trigger.

@sample chime  url("/sfx/chime.wav");
@sample crunch url("/sfx/crunch.mp3");

button[data-action="confirm"] { sound-on-click: chime; }
button[data-action="delete"]  { sound-on-click: crunch; volume: 0.7; }

Sample volume is normalized through the same calibration pipeline as procedural presets (first-trigger latency: ~30 ms while it caches).

Mood overlays

A sound-mood is an inheritable filter that paints over any sound triggered on the element or its descendants. Nine built-in moods ship; mix wet/dry with sound-mood-mix.

MoodEffect
warmSoft lowpass + gentle saturation
brightHigh-shelf boost + lowpass relief
glassyResonant peak around 4–6 kHz
metallicComb filter at audible delays
organicSlow LFO on cutoff + mild detune
punchyTransient enhancer + tight gate
retroBandpass + bitcrush
airySubtle high-shelf + reverb send
lofiLowpass + sample-rate reduction + wow/flutter
.retro-app { sound-mood: lofi; }
.lab        { sound-mood: glassy; sound-mood-mix: 0.4; }   /* 40% wet */

Rooms (reverb)

Each element resolves one room — the cascaded room value. The runtime maintains lazy convolver chains keyed by room name; first use instantiates, subsequent triggers reuse.

RoomTail (s)Use case
dry0Default; bypasses convolver
small-room0.4UI taps, modals, intimate alerts
medium-room0.9Default for most apps
large-room1.6Concert / cinematic feel
hall2.8Long, lush, ambient
plate1.2Dattorro plate — classic studio reverb

CSS-var bridge

Any ACS value can read a CSS custom property via var(--token). The runtime resolves against getComputedStyle(documentElement) at trigger time — so dark/light theme switches are picked up live.

:root          { --beep: 880hz; }
button.primary { sound: my-tone; }

@sound my-tone {
  body { tones: var(--beep); decay: 120ms; }
}

String declarations (sound, room, mood) and numeric ones (volume, pitch, frequencies) all resolve through the same bridge.

@media & accessibility

ACS supports the same @media nesting as CSS, with two ACS-specific media features:

@media (prefers-reduced-sound: reduce) { :root { master-volume: 0; } }
@media (input-modality: keyboard)      { button:on-focus { sound: tick; } }

prefers-reduced-sound is still a CSS proposal — the runtime evaluates it gracefully (always false) until browsers adopt it. Your stylesheet stays correct; users with screen readers / reduced-motion preferences won't hear extras when the media query lands.

VSCode & Cursor extension

Installs from both major marketplaces. Same package ID (audio-cascading-style-sheets.acs-language), same version everywhere.

Features

  • TextMate grammar — strings, numbers, properties, at-rules, selectors all coloured.
  • Document outline (DocumentSymbolProvider) — every @sound appears in the outline panel.
  • Folding ranges for @sound bodies, @media blocks.
  • Hover docs for every property, preset, room, mood.
  • Live linter — "unknown property 'master-volum' — did you mean 'master-volume'?"
  • Audition CodeLens — click above any @sound to hear it.
  • 🔊 Open in Picker CodeLens — opens the interactive layer editor.
  • 30+ snippets covering bell / FM-bell / snare / kick / pluck / fast-tap recipes plus cascade blocks for buttons / toasts / switches / inputs / dialogs.

Sound picker

The picker is a webview shipped inside the extension. Two modes:

  1. Browser — intent-based gallery with hover-preview, keyboard nav, ASCII waveform thumbs.
  2. Layer editor — knob-based DSL editor for the @sound block of any preset. Source dropdown, freq, ratios/decays/gains text inputs, FM modulator, pitch-from sweep, detune, brightness (pluck), filter + cutoff, attack/decay, gain/drive/pan/start. Drag-to-reorder layers; live waveform; Reset to original; Copy as @sound for paste-ready output.

Open via the editor title-bar 🔊 icon while in an .acs file, or run "ACS: Open Sound Picker" from the command palette.

CLI tools

Three Node scripts ship with the npm package — pure ESM, no transpile.

Lint

npx acs-audio/lint poc/*.acs
# or
node node_modules/acs-audio/tools/lint-acs.mjs my-style.acs

Validates property names, preset references, mood names, room names. Fuzzy-match suggestions for typos.

Format

node node_modules/acs-audio/tools/format.mjs my-style.acs

Pretty-prints in-place. Aligns properties, normalizes whitespace, sorts @sound blocks alphabetically by preset name (optional).

Compile

node node_modules/acs-audio/tools/compile-acs.mjs --pretty --out style.json my-style.acs

Pre-parses to a JSON AST. Useful for build-time inlining or shipping a minified runtime that skips the parser.

Skills (AI agents)

ACS ships a Claude / Cursor "skill" that lets an AI agent design a sound for you from a natural-language prompt or by reverse-engineering an audio file. The skill sits in skills/create-acs-sound/ in the repo.

What it does

  • Prompt input — "Give me a soft pop that's brighter than the default" → emits a calibrated @sound block.
  • Audio input — Drop a .wav, the skill measures source/envelope/effects and reverse-engineers a matching @sound.
  • Mixed — Audio + qualifier ("warmer", "punchier") refines the measured definition.
  • Round-trip render — The skill renders the result to a WAV preview and validates within tolerance bands (gain budget, frequency bounds, duration cap).

Install

One command via the skills CLI (vercel-labs) — supports Claude Code, Cursor, Codex, OpenCode, Gemini CLI, GitHub Copilot, Warp, Windsurf, and 50+ other agent runtimes:

npx skills add Grkmyldz148/acs-skills

The CLI prompts you to pick which agent(s) to install for, then drops the skill into their conventional path (e.g. .claude/skills/create-acs-sound/ for Claude Code, .cursor/skills/... for Cursor, etc.).

Grkmyldz148/acs-skills is an auto-mirror of the skill source in the monorepo — a GitHub Action pushes the snapshot on every change to main.

Manual install (fallback)

# clone the repo
git clone https://github.com/Grkmyldz148/acs.git
# copy the skill into your agent's skills directory:
cp -r acs/skills/create-acs-sound ~/.claude/skills/
# or reference SKILL.md directly:
acs/skills/create-acs-sound/SKILL.md

The skill is a directory of rules/*.md atomic decision documents plus pipeline orchestration in SKILL.md. Compatible with the vercel-labs skills CLI ecosystem (Claude Code, Cursor, Codex, OpenCode, and 50+ others).

Pipeline

1. pipeline-detect-input    → prompt | audio | both
2. pipeline-pick-base-layer → match event-class tokens (event-click, event-tap, …)
3. pipeline-apply-mood      → adjective tokens mutate source/filter/effects
4. pipeline-decide-layering → 1, 2, or 3 layers based on event class
5. pipeline-emit-and-render → emit @sound, optional WAV preview, validate

Authoring your own skill

Each rule is a markdown file with frontmatter declaring its category (event, layer, mood, effect, pipeline, validate, interpret) and a body containing the rule's logic + an example @sound block. Run node src/build.mjs to compile SKILL.md from the rules.

See the skills directory in the repo for the full source.

Programmatic API

Most use is declarative — these are escape hatches.

Trigger directly

window.ACS.trigger({ sound: "pop", pitch: "+2st", volume: 0.7 }, "click");

Set master config

window.ACS.setMasterConfig({
  "master-volume": 0.7,
  room: "small-room",
  quality: "high",
});

Watch a stylesheet

window.ACS.watch("/style.acs", { interval: 1000 });
// re-fetches + re-binds on changes — useful in dev

Subscribe to triggers

const unsubscribe = window.ACS.onTrigger(({ preset, decls, eventKey, target }) => {
  console.log(preset, eventKey, target);
});

Devtools overlay

window.ACS.devtools.mount();
// fixed-position panel: last 20 triggers + preset / source / factor / mood / room
window.ACS.devtools.unmount();

The overlay subscribes via onTrigger, so the cost when not mounted is zero.

Framework helpers

window.ACS.helpers.useSound takes a hooks injection so the runtime has no React peer dep. Works with any hook-shaped library.

import { useCallback } from "react";

const ding = window.ACS.helpers.useSound({ useCallback }, "ding");
<button onClick={ding}>Notify</button>

Other helpers:

window.ACS.helpers.play("pop", { pitch: "+1st" });
window.ACS.helpers.attach(myButton, "ding", "click");

Custom melodies — deep dive

This section is for authors who want to write @sound blocks from scratch instead of using the 49 built-ins. The mental model is the same one used by the create-acs-sound skill — read it once and you'll understand both.

Step 1 — Pick an archetype

You wantUse
Bell-like dingtones: f1, f2, f3 with descending decays
Wood / drum knockmodal: f, ratios: 1 r2 r3, decays: d1 d2 d3
Plucked stringpluck: f, brightness: 0..1, decay: t
Filtered noise (snare/clap/breath)noise: white, filter: bandpass, cutoff: f, q: q
FM bellosc: sine, freq: f, fm: { ratio: r, depth: d }

Step 2 — Author the body layer

@sound my-sound {
  body {
    /* source key */
    tones: 880hz, 1320hz, 2200hz;

    /* envelope */
    decays: 0.6s, 0.4s, 0.25s;        /* per-tone if multi */
    attack: 4ms;                       /* default 0 */

    /* level */
    gain: 0.4;                         /* before calibration */
  }
}

Step 3 — Stack a click layer

Most "satisfying" UI sounds are body + click. The body gives the tonal identity; the click gives the sharp transient.

@sound my-tap {
  body  { tones: 1.4khz; decay: 50ms; gain: 0.4; }
  click { noise: white; filter: highpass; cutoff: 4khz; decay: 8ms; gain: 0.3; }
}

Step 4 — Validate

  • Gain budget: sum of layer gain values should be ≤ 1.
  • Frequency bounds: keep fundamentals between 80 Hz – 8 kHz for UI sounds.
  • Duration cap: tail under 0.5 s for taps, under 1.5 s for toasts/notifications.
  • Run the linternpx acs-audio/lint your-style.acs.
  • Audition — ▶ CodeLens above the @sound block in VSCode.

Step 5 — Calibrate (optional, advanced)

ACS's calibration pipeline assigns each preset a BAKED_FACTOR so volume: 0.5 sounds equally loud across the library. For your own presets the runtime calibrates on-the-fly at first trigger, but you can pre-bake:

node node_modules/acs-audio/analyzer/defaults-loudness.mjs --bake my-style.acs

This writes a JSON sidecar with K-weighted RMS measurements; the runtime reads it on load and applies the factor before volume.

Layer keys reference

Complete list of keys you can put inside any layer block.

Source (pick one)

KeyValueNotes
tonesComma-separated frequenciesAdditive sine bank
modalFrequencyPair with ratios, decays, gains
oscsine | triangle | square | sawtoothPair with freq
pluckFrequencyPair with brightness, decay
noisewhite | pink | brownPair with filter

Modulation / shaping

KeyValueApplies to
fm{ ratio, depth }osc
pitch-fromstart to endSweep
detuneCentsosc
filterlowpass | highpass | bandpass | tpt-*noise / osc
cutoffFrequencyfilter
qNumberfilter (resonance)

Envelope

KeyValue
attackms / s
decayms / s
releasems / s
decaysComma-separated (per partial)

Output

KeyValue
gain0..1
drive0..1 (saturation)
pan-1..1
startOffset (ms / s) — delays this layer
realtimetrue — route through AudioWorklet for sub-1ms latency

Built-in preset library (49)

Auto-loaded as defaults.acs. All calibrated; cross-preset spread 4.8 dB. Override any by re-defining @sound <name> in your stylesheet.

UI taps & clicks

tap · tap-tactile · click · click-soft · tick · pop · thunk · woodblock

Bells & pings

bell · bell-soft · bell-bright · chime · chime-soft · ding · ping · ting

Drums & percussion

kick · snare · hat · clap

States & flow

success · complete · confirm · error · denied · buzz · notify · mention · badge · prompt

Modals & menus

modal-open · modal-close · drawer-open · drawer-close · dropdown-open · dropdown-close · page-enter · page-exit

Toggles & transitions

toggle-on · toggle-off · swoosh · whoosh · carriage-return

Texture & ambient

glass · gong · string · sparkle · pluck-soft · pluck-bright

Theme packs (8)

Drop-in stylesheets that re-bind the entire UI vocabulary.

ThemeVibe
apple.acsRestrained UI taps, modal-open like macOS
material.acsPunchy, slightly tonal, ripple-style
retro.acs8-bit blips, CRT-flavored
brutalist.acsIndustrial thunks, no decorum
cinematic.acsLong tails, sub-rumbles
bauhaus.acsClean tones, primary-color UI mapped to fundamentals
terminal.acsDry typewriter clacks + carriage returns
ambient.acsDiffused chimes, all-medium-room
<link rel="audiostyle" href="https://cdn.jsdelivr.net/gh/Grkmyldz148/acs@main/poc/themes/apple.acs" />

Browser support

Anything with the Web Audio API — Chrome, Firefox, Safari ≥ 14, Edge.

FeatureChromeFirefoxSafariEdge
Core (<link rel=audiostyle> + cascade)✅ 14+
AudioWorklet (realtime: true)✅ 14.1+
prefers-reduced-sound (proposal)
var(--token) resolution

Troubleshooting

Nothing plays on first interaction

Browsers block AudioContext until user gesture. The runtime auto-resumes on pointerdown / keydown; the first click of the session may be silent if it lands before the resume callback. Open devtools, check window.ACS.audioContext.state — should be "running" after one interaction.

Some buttons silent, others not

Cascade specificity: a more-specific selector with an unknown preset name silently overrides a working base rule. Run the linter — npx acs-audio/lint my-style.acs — it catches typo'd preset names.

Sounds clipping / too loud

Lower master-volume on :root. The limiter prevents distortion but won't make peaks pleasant. If a specific preset is too loud, override its baked factor via volume: X !raw.

Worklet errors in console

The runtime falls back to the main thread automatically. Set realtime: false on offending layers to suppress the warning, or open an issue with your browser/version.

Stylesheet edits don't refresh

The runtime fetches once. In dev, opt-in to live-reload: window.ACS.watch("/my-style.acs", { interval: 1000 }).

FAQ

Does ACS depend on PostCSS?

No. ACS is a separate language with its own parser. .acs files are served as-is and parsed in the browser by the runtime. There's no build step, no preprocessor, no relationship to CSS tooling.

Can I use it with React / Vue / Svelte?

Yes — the runtime is framework-agnostic. npm install acs-audio, then import "acs-audio" once at the entry point. Selectors target DOM elements regardless of which framework rendered them.

Does it work with server-side rendering?

The runtime is browser-only — it imports cleanly under SSR (no top-level DOM access) but does its work after hydration. Add <link rel="audiostyle"> in your document head and the runtime takes over once the page is interactive.

Is there a way to disable all sounds at runtime?

Yes — window.ACS.setEnabled(false) mutes everything without unmounting. setEnabled(true) resumes. Or via stylesheet: :root { master-volume: 0; }.

Why "cascading style sheets" for audio?

Because the cascade is the right abstraction. UI sound design is a cross-cutting concern — buttons sound a certain way, primary buttons louder, primary buttons inside dialogs softer. CSS is exactly the language for "rules that compose through specificity and inheritance." Re-using its mental model (and its tooling — see the VSCode extension) lets authors transfer 95% of what they already know.

Can I mix .acs files from multiple sources?

Yes. The runtime loads defaults.acs first (built-in presets), then every <link rel="audiostyle"> in document order. Later wins on ties; specificity wins always. You can drop a theme pack and override just the bits you want in your own file.

How do I attribute the project?

MIT license, attribution welcome but not required. If you do credit, link to audiocss.dev or github.com/Grkmyldz148/acs.

Copied to clipboard