Open-source project
kikkupico/ambientcss avatar
kikkupico/ambientcss

Ambient CSS: lighting a web page with one light vector instead of five shadow utilities

A physics-based lighting system for CSS. Define a light source, and every shadow, highlight and surface gradient follows from it β€” calibrated against Blender raytraces.

586 stars24 forksTypeScriptMIT

At a glance

What is it?
Ambient CSS is a CSS framework that treats interface surfaces as physical matter under a two-light setup, so a single light direction on a container produces every shadow, highlight and gradient beneath it. The five-layer box-shadow construction is calibrated against raytraced Blender reference models, and the same idea carries into a React component library with composable control mechanisms.
Who is it for?
Adopt Ambient CSS if you are building a tactile, skeuomorphic interface and want a coherent lighting model instead of hand-picked shadow utilities, particularly if you can accept that the look is defined by one light environment rather than per-component tweaks. Do not adopt it for a flat, product-dashboard aesthetic, because every element is painted as a lit physical substance and that is not a neutral default.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 41 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The complaint behind the project: shadows that do not share a scene

The argument for Ambient CSS starts with a criticism of the standard approach. Traditional CSS shadow scales are decorative, and the README puts it concretely: a card with shadow-lg next to a button with shadow-sm does not imply a shared physical scene. They are just two unrelated blur values coexisting on the same page.

That is a fair observation, and it is the kind of thing that only bothers you once you have built an interface where every panel looks like it came from a different product. The usual shadow scale has no consistent answer to where the light is coming from, so each component's shadow was chosen by eye and nothing lines up.

Ambient CSS starts from physical lighting principles instead. You describe a lighting environment, and the light direction, the key and fill intensity, the hue and the elevation are the inputs. All shadows, edge highlights and surface gradients follow deterministically from that description.

The payoff is stated as a property of the mechanism rather than of the aesthetics. Change the light vector on a container, and every child element updates consistently.

That is the difference between a theme file and a lighting model. A theme file gives you a set of values; a lighting model gives you a function from scene parameters to pixels, and every element in the scene is evaluated against the same function.

The project is MIT licensed, written in TypeScript, and the last push was on 2026-09-13. There are no GitHub releases, and the packages are published to npm as @ambientcss/css and @ambientcss/components, with a live demo and a documentation site.

Five shadow layers, and the five surface concerns they come from

The engine models UI surfaces as physical matter under a two-light system, a key light and a fill light. Every element reads shared CSS custom properties and generates a five-layer composite box-shadow.

The five layers are, in order: the drop shadow, a directional umbra and penumbra scaled by physical elevation; the fillet highlight, a specular inner edge highlight facing the light source; the fillet shadow, a soft inner shadow on the opposite edge; the chamfer highlight, a wide bevelled inner glow along lit facets; and the chamfer shadow, a dark bevel shadow on shadowed edges.

None of that is available in CSS directly. A box-shadow property with a comma-separated list can take multiple shadows, and those five entries are what a bevelled, lit object actually looks like when flattened into a 2D box. The four inner ones are inset shadows, which is how a bevel is expressed without any real geometry.

The substrate side of the model has five concerns, and the README lays them out:

code
Structure ─── ambient               (enables lighting calculations)
Surface ───── flat / concave / convex (background lighting gradient)
Edge ──────── chamfer / fillet / groove (beveled & rounded edge cuts)
Material ──── matte / shiny / glass  (specular reflection & translucency)
Depth ──────── elevation 0–3          (drop shadow height)

Structure is the switch that turns on the lighting calculations, so a plain element is unaffected until you opt in. Surface chooses between a flat fill, a concave dish and a convex bulge, each of which produces a different background gradient under the same light. Edge picks the cut: chamfer for a hard bevel, fillet for a rounded edge, groove for a channel. Material decides how the surface reflects. Depth is the elevation scale, 0 to 3, and it is what sets the drop shadow height.

The lighting behaviour is described as physically grounded and calibrated against raytraced 3D reference models built with Blender, in a separate ambient3d directory that acts as the ground truth. The README's hero image makes the point visually: the same hardware panel, raytraced in Blender on one side of a wipe and rendered live in the DOM on the other, with the same geometry and the same light vector, box-shadow instead of Cycles.

Setting up a light environment in plain CSS

The pure CSS install is a single package, and the README is emphatic that it has zero dependencies and works with any framework or plain HTML:

bash
npm install @ambientcss/css

Then you include the stylesheet and mark up a light container with an element inside it:

html
<link rel="stylesheet" href="node_modules/@ambientcss/css/dist/ambient.css" />

<!-- Set up light environment on any parent -->
<div class="amb-light-tl">
  <button class="ambient amb-surface amb-chamfer amb-elevation-1 amb-rounded">
    Click me
  </button>
</div>

The light direction is set by a class on any ancestor element and is inherited by all descendants. The available classes are amb-light-tl, amb-light-tr, amb-light-bl, amb-light-br, amb-light-top, amb-light-bottom, amb-light-left and amb-light-right, the four corners plus the four edges.

For finer control there are custom properties, and the README gives them with their ranges:

css
.my-scene {
  --amb-light-x: -0.7;               /* Horizontal (-1 left, 1 right) */
  --amb-light-y: -0.8;               /* Vertical (-1 top, 1 bottom) */
  --amb-key-light-intensity: 0.9;    /* Primary directional light (0..1) */
  --amb-fill-light-intensity: 0.6;   /* Ambient fill light (0..1) */
  --amb-light-hue: 234;              /* Light hue (0..360) */
  --amb-light-saturation: 15%;       /* Light saturation */
}

The two-light structure is the part that matters for the look. The key light is the directional one that produces the crisp shadow and the specular highlight, and the fill light is the ambient term that keeps the shadow side from going black. The hue and saturation apply to the light itself, which is how a scene picks up a colour cast.

The surface classes are amb-surface, amb-surface-concave, amb-surface-concave-h and amb-surface-convex, and the edge classes are amb-chamfer, amb-chamfer-2, amb-fillet, amb-fillet-2 and amb-groove.

Albedo and shade: one colour law instead of five fixed classes

The part of the design that will affect your existing markup most is the surface colour model, and it is a deliberate simplification of an earlier version.

A surface is its substrate under the scene's light, not a fixed colour, so one class covers every hue and shade:

css
.my-panel {
  --amb-albedo: crimson;   /* the colour under full illumination */
  --amb-shade: 1;          /* multiplier on that reflectance */
}

Both variables inherit, and that inheritance is the mechanism. A panel colours everything inside it, because the grooves, the dishes and the components package's knobs and keys all read the same lit tone. Dim the key light and a crimson panel darkens like crimson. Give the lamp a hue and it takes the cast.

That is the opposite of how design systems usually work. A typical system gives you a palette of fixed values and you reference them; this one gives you a reflectance and lets the lighting model compute the appearance. The consequence is that the same albedo looks different under a different light, which is either the point or a problem depending on whether you want your brand colour to be literal.

There is a migration note for anyone who used the earlier API. The former amb-surface-darker, amb-surface-darkest, amb-surface-lighter and amb-surface-lightest classes were five fixed albedos of this one law. The equivalent shades are 0.38, 0.07, 1.11 and 1.16, so you can translate an old class into a shade multiplier rather than hard-coding a colour.

The examples directory shows how the framework is meant to be dropped into different setups, with folders for css-basic, css-purecss, css-tailwind, react-ambientcss, react-basic, react-bootstrap and react-tailwind, which is a useful signal that the author expects it to coexist with Tailwind and with plain HTML rather than replace them.

The React package: presets that own mechanisms, parts that own paint

The React package installs alongside the CSS one:

bash
npm install @ambientcss/components @ambientcss/css

The usage is a provider carrying the lighting theme, with components inside it:

tsx
import { AmbientProvider, AmbientButton, AmbientKnob, AmbientPanel } from "@ambientcss/components";
import "@ambientcss/css/ambient.css";
import "@ambientcss/components/styles.css";

function App() {
  return (
    <AmbientProvider theme={{ lightX: -1, lightY: -1, keyLight: 0.9, fillLight: 0.7 }}>
      <AmbientPanel>
        <AmbientButton>Press</AmbientButton>
        <AmbientKnob label="Gain" defaultValue={50} />
      </AmbientPanel>
    </AmbientProvider>
  );
}

The theme object on the provider is the React spelling of the same light parameters the CSS package uses as custom properties, with lightX, lightY, keyLight and fillLight.

The interesting design decision is how the components are structured. Each component is a preset: a mechanism that owns the kinematics, the value and the ARIA, paired with a set of parts that own the paint. The README states the consequence plainly: reach past the preset and the mechanism will wear whatever you draw.

tsx
<AmbientRotary
  value={gain} onChange={setGain}
  travel={240} input="drag"
  parts={{ base: <KnobBody flush />, actuator: <MyPointer /> }}
/>

So the Rotary component owns the drag handling, the value binding and the accessibility attributes, while you supply the base and the actuator. A control publishes --ambx-percent, --ambx-angle and --ambx-size on its own root, so a part can be pure CSS with no React involvement at all.

That split is what makes the library usable for hardware-style interfaces rather than only for the presets. A knob is a rotary control with a pointer, a travel range and a value, and those are the same concerns whether the knob is for a mixing desk or a rack-mounted synth. The presets in the demo application are described as grounded hardware presets, and the demo is a hardware synthesizer and audio gear application.

The monorepo, the release process and the open question

The repository is a pnpm workspace, and the package layout is worth knowing because the pieces have very different characters.

There are five entries in the package table. @ambientcss/css is the pure CSS lighting framework, with zero dependencies, working with any framework or plain HTML. @ambientcss/components is the tactile React component library. ambient3d is a parametric Blender 3D component kit and ground-truth raytracing calibration engine, which is the reference side of the project. There is a docs package, an interactive Docusaurus documentation website and live code playground, and a demo package, the hardware synthesizer and audio gear live demo application.

The tooling in the root package.json shows the release discipline. The package manager is pnpm at 10.2.1, and the build, clean and typecheck scripts all delegate to a shared workspace task runner script. There are separate scripts for the docs site and for the demo, and a release:check script that runs an API check, a build, a typecheck and a docs build in sequence.

Publishing goes through changesets, with a version-packages script and a release:changeset:publish script that runs a preflight step, typechecks both packages and then calls a publish-trusted script. The publish commands use the public access flag and no-git-checks, which is consistent with a trusted publishing setup where the release is authorised outside the local git state.

There is also a hero image pipeline with its own scripts, hero:shot and hero:gif, which builds the package that produces the screenshot and then assembles the animation with a Python script. That is a level of automation around one marketing asset that most projects do not have, and it is a reasonable proxy for how seriously the visual claims are meant to be taken.

The open question for an adopter is scope. The framework is opinionated about appearance. Every element it touches is painted as a lit physical substance with a bevel, a specular edge and a cast shadow, and the whole design leans towards a skeuomorphic or hardware aesthetic. The topic list includes skeuomorphism, design-system, box-shadow and react, which is an accurate summary. If your product is a flat dashboard, this is a system you would be arguing with rather than adopting, and the effort goes into choosing light directions that make it look less like a lighting model.

Editorial conclusion

Adopt Ambient CSS if you are building a tactile, skeuomorphic interface and want a coherent lighting model instead of hand-picked shadow utilities, particularly if you can accept that the look is defined by one light environment rather than per-component tweaks. Do not adopt it for a flat, product-dashboard aesthetic, because every element is painted as a lit physical substance and that is not a neutral default. Before you start, pick the light direction classes and the elevation scale deliberately, set the albedo and shade variables rather than per-element colours, and read the release documentation for the CSS package, since the class names changed when the fixed surface shade classes were replaced by the single albedo and shade law.

Frequently asked questions

What is Ambient CSS and what problem does it solve?

It is a CSS framework that derives every shadow, highlight and surface gradient from a single described lighting environment, so elements on a page share one physical scene instead of using unrelated hand-tuned shadow utilities. The README's complaint is that a card with shadow-lg next to a button with shadow-sm implies no shared light source.

How does Ambient CSS render a bevelled, lit element?

It generates a five-layer composite box-shadow: a drop shadow with umbra and penumbra scaled by elevation, a fillet highlight facing the light, a fillet shadow on the opposite edge, a chamfer highlight along lit facets and a chamfer shadow on shadowed edges. The behaviour is calibrated against raytraced reference models built in Blender.

How do I set the light direction in Ambient CSS?

Set a light class such as amb-light-tl, amb-light-tr, amb-light-bl, amb-light-br, amb-light-top, amb-light-bottom, amb-light-left or amb-light-right on any ancestor, and it is inherited by descendants. For finer control use the custom properties --amb-light-x, --amb-light-y, --amb-key-light-intensity, --amb-fill-light-intensity, --amb-light-hue and --amb-light-saturation.

What replaced the amb-surface-darker and amb-surface-lightest classes?

They were five fixed albedos of the single albedo and shade law, which is now expressed with the --amb-albedo and --amb-shade variables. The equivalent shades for the removed classes are 0.38, 0.07, 1.11 and 1.16.

How does the Ambient CSS React package let me customise a control?

Each component is a preset that owns the kinematics, the value and the ARIA, paired with parts that own the paint. You pass your own parts to a control such as AmbientRotary, and the control publishes --ambx-percent, --ambx-angle and --ambx-size on its root so a part can be pure CSS.

Official sources

  1. Issues
  2. kikkupico/ambientcss on GitHub
  3. License: MIT
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/kikkupico-ambientcss.svg)](https://hysenlabs.com/projects/kikkupico-ambientcss)

Was this useful?