Using LLMs to Generate UI References — and Teach Other LLMs
This blog was generated by Claude, and approved by a human being.
How I built a neumorphic design system with Claude, then used it to make every future conversation smarter
There's a workflow I've been quietly using that I think more people should know about. It's not complicated, but it took me a while to fully understand what I was actually doing — and why it works so well.
The short version: you can use an LLM to generate a living reference artifact, then feed that artifact back to other LLMs (or the same one) as a source of truth. The result is a tighter feedback loop, more consistent output, and a kind of institutional memory that doesn't rely on the model already knowing what you want.
I'm going to use neumorphic UI design as the concrete example throughout this post, because it's a domain where LLMs frequently get things wrong in instructive ways — and fixing those mistakes teaches you a lot about how to do this right.
The Problem: LLMs Know About Things, Not Always How to Do Them
When I first asked Claude to generate neumorphic UI components, the output was recognizable but off. The shadows were too harsh. The lighting was directionally inconsistent — some elements lit from the top-left, others from nowhere in particular. The shadow colors were pure rgba(0,0,0,0.25) and rgba(255,255,255,0.9) overlays, which look fine on neutral grey but fall apart completely on a tinted base color like lavender or sage green.
The model knew what neumorphism was. It could explain the design philosophy accurately. But knowing about something and being able to reliably execute it are different things — for humans and for AI alike.
The issue isn't that the model is bad. It's that neumorphism is a system, and systems require constraint. Left to its own judgment, a model will make locally reasonable choices that don't cohere into a globally consistent result. Each shadow decision seems fine in isolation. Together, they don't add up to a physical material.
This is the exact problem a well-constructed reference file solves.
What a Reference File Actually Is
A reference file, in this context, is a document you create once — with the model's help — that encodes your design decisions as constraints. It's not documentation you write by hand. It's a generated artifact that captures the outputs of a successful session, so that future sessions can reproduce them consistently.
Think of it less like a spec sheet and more like a worked example. A spec says "shadows should be directional." A reference shows a working implementation where they are, gives the model the exact CSS variables, explains why each decision was made, and demonstrates what correct output looks like across a range of components.
The neumorphic reference I built with Claude ended up containing:
- A shadow system with three elevation levels (
--neu-sm,--neu-md,--neu-lg), each using exactly two shadows — one light at-x, -yand one dark at+x, +y - An inset system that reverses the direction for sunken surfaces
- Shadow colors derived as actual RGB tints of the base color (not white/black overlays)
- A JavaScript theme engine that recalculates shadow colors when the base changes
- Every major UI widget — toggles, buttons, inputs, selects, sliders, checkboxes, tabs, lists, scroll areas, modals, toasts — all in a single self-contained HTML file
That last part matters more than it might seem. Having all the widgets in one file means the model can see how they relate to each other. It's not just a button. It's a button that belongs to the same surface system as the input field next to it, the tab bar above it, and the card containing them all.
How the Session Actually Went
This is the part that's easy to skip over but really shouldn't be.
The first output wasn't the reference. It was a draft. And the way I got from draft to reference was by doing a structured peer review — asking the model to critique its own work — before asking it to revise.
Here's what I asked, roughly: "Do a peer review of your own code. The lighting and edges are too abrupt. You got buttons mostly right, but they could use work. Apply what you know about neumorphism to get the most realistic, smooth look."
The model's self-review identified the core problems clearly:
- The
--insetvariable had no directional origin — it usedinset 0 0 35px, a centered bloom, which breaks the entire physical lighting metaphor - The raised shadow used six layers that fought each other, when correct neumorphism needs exactly two
- Shadow colors were pure rgba overlays, not base-color-derived tints
- The toggle socket and button used incompatible lighting models on the same element
This process — generate, review, revise — is the core of the workflow. You're not just asking for output. You're using the model's reasoning capacity to catch its own production errors, then using that analysis as the prompt for the next version.
The revised file was substantially better. But more importantly, it became the source of truth.
The Shadow System, Explained
Before going further, I want to explain the actual design principle that makes this work — because understanding it is what lets you write prompts that produce correct results, and evaluate whether what you got back is right.
Neumorphism simulates physical material under a single directional light source. The standard convention is top-left. This means:
- Raised surfaces cast a light shadow toward the upper-left (negative x, negative y) and a dark shadow toward the lower-right (positive x, positive y)
- Sunken surfaces reverse this: the dark shadow falls in the upper-left corner (inside the well) and the light shadow falls in the lower-right
The CSS for a raised element looks like this:
box-shadow:
-6px -6px 16px var(--sh-light),
6px 6px 16px var(--sh-dark);
And for an inset element:
box-shadow:
inset 4px 4px 10px var(--sh-dark),
inset -4px -4px 10px var(--sh-light);
That's the whole system. Two shadows. Direction consistent with a single light source at top-left. Everything else in neumorphic design is a variation on this.
The second key insight is shadow color derivation. rgba(0,0,0,0.25) looks fine on #ccd0d4. But apply that same shadow to #e4dff0 (lavender) and you get a muddy grey cast that completely loses the tint. The correct approach is to compute shadow colors as actual RGB tints of the base:
// Light shadow: base + 30 on each channel
const shLight = `rgb(${r+30}, ${g+30}, ${b+30})`;
// Dark shadow: base × 0.80 on each channel
const shDark = `rgb(${Math.round(r*0.80)}, ${Math.round(g*0.80)}, ${Math.round(b*0.80)})`;
When you do this, the shadows carry the hue. A sage green base gets green-tinted shadows. A rose base gets warm shadows. The result looks physically plausible in a way that black/white overlays never do.
Using the Reference File in Future Sessions
Once the reference file exists, the workflow for any new session becomes straightforward.
Option 1: Attach it directly. Most LLM interfaces let you upload files. Drop the HTML file into the conversation and say: "This is my neumorphic reference. Build me a [date picker / kanban card / notification panel] that matches this shadow system exactly. Don't invent new shadow values — derive them from the same --sh-light / --sh-dark variables."
The model can read the CSS variables, understand the shadow stack, and apply it to the new component without guessing. It has a working example to pattern-match against, not an abstract principle to interpret.
Option 2: Reference the system verbally with precision. Even without the file, having built it once means you can describe the system exactly: "Use a two-shadow raised style with --neu-md: -6px -6px 16px var(--sh-light), 6px 6px 16px var(--sh-dark). Shadow colors are RGB tints of the base, not rgba overlays. Inset reverses direction. Pressed is a shallow inset with a reduced light shadow."
That level of specificity produces dramatically more consistent results than "make it neumorphic."
Option 3: Use the code reference block. The reference file itself contains a documented CSS variable block — a compact version of the shadow system with inline comments explaining what each token does and why. This block is designed to be paste-able into any new prompt as a constraint. You're giving the model its own previous correct output as a constraint on the new one.
Teaching Other Models
This is the part that scales.
The neumorphic reference isn't just useful for Claude. It's useful for any model that will render UI. Smaller, faster models — the ones you'd use for quick code generation or autocomplete in an IDE — often don't have the contextual depth to reason about a design system from first principles. But they can absolutely follow a worked example.
When you use a reference file as context for a smaller or more constrained model, you're essentially doing the reasoning work once (with a more capable model) and distributing the result (to any model that can read it). The reference does the heavy lifting. The downstream model just needs to pattern-match against it.
A few practical notes on this:
Keep the reference file self-contained. No external imports, no dependencies. The HTML file should render correctly when opened locally, with no network calls required for the design system itself. This makes it portable across any environment where the model can read files.
Include the reasoning, not just the output. The code reference block in the file explains why shadows use RGB tints instead of rgba overlays, not just that they do. A model reading "don't use rgba overlays because they lose the hue on colored bases" is far less likely to regress than one reading only a list of CSS values.
Version the reference. As your design evolves, the reference should too. Keep old versions around — they're useful for understanding what changed and why, and for rolling back if a revision breaks something.
Structure it so a model can navigate it. The reference file uses clearly labeled sections with consistent naming (--neu-sm, --neu-md, --neu-lg — small, medium, large). This isn't just for human readability. Models parse structure. A well-labeled token system is easier to apply correctly than an undifferentiated block of CSS.
The Broader Pattern
Neumorphism is the example, but the pattern generalizes to anything with a coherent underlying system.
Data visualization libraries. Animation easing systems. Typography scales. Spacing grids. Color palette derivation. Component composition patterns. State machine logic for UI flows. All of these have underlying rules that are hard to convey abstractly but easy to demonstrate concretely.
The workflow is:
- First pass: Ask the model to generate a comprehensive example. Don't worry too much about correctness yet.
- Review pass: Ask the model to critique what it made. Be specific about the domain. ("Review this for physical lighting consistency" is more useful than "is this good?")
- Revision pass: Use the critique to produce a corrected version. This is your reference.
- Document the system explicitly. Add a section to the reference that explains the rules in compressed, machine-readable prose. Not a tutorial — more like a constraint spec.
- Embed the reference into future sessions as context, attachment, or paste.
- Iterate. Every time you find an edge case the reference doesn't cover, add it. The reference grows more precise with use.
The model is doing the work, but you're directing the quality feedback loop. That's a fundamentally different relationship than just prompting and hoping.
A Note on Honesty About What This Is
I want to be clear about something: this workflow doesn't eliminate mistakes. It reduces them and makes them more addressable.
When you have a reference, a mistake is no longer "the model doesn't understand neumorphism." It's "the model deviated from the reference in this specific way." That's a tractable problem. You can point to the deviation, ask why it happened, and correct it. Without a reference, you're re-litigating first principles every session.
The reference also gives you a place to put the knowledge you accumulate. Every time you catch a mistake — a centered shadow where a directional one was needed, an rgba overlay where a tinted shadow was correct — you can add a note to the reference explaining what went wrong and why the correct version is correct. That note will be there for the next session, and the one after that.
Over time, the reference becomes a record of every interesting mistake the system has made and how it was resolved. That's more valuable than most design documentation I've seen.
Getting Started
If you want to try this with the neumorphic example specifically, the reference file is embedded in this post. Open it locally, pick a base color and accent, and spend a few minutes interacting with the widgets. Get a feel for what the system looks like when it's working correctly.
Then try this prompt with any model:
"I'm attaching a neumorphic UI reference file. Build me a [component] using the same shadow system. The key constraints are: exactly two shadows per surface (one light at -x,-y, one dark at +x,+y), shadow colors derived as RGB tints of the base using
--sh-lightand--sh-dark, inset surfaces reverse direction, pressed state uses a shallow inset. Don't invent new shadow values."
See what comes back. See where it deviates. Add those deviations to your reference.
That's the loop. It's not complicated, but it compounds.
You may as well ask it to critique this reference. For example:
"Does this reference work with different colors? Especially the light shadows. Do a review of that specific angle and report the mistakes made in their while suggesting fixes for them".
The neumorphic reference file used throughout this post is available for download. The shadow system, theme engine, and all widget implementations are in a single self-contained HTML file, no dependencies required.
Comments
Post a Comment