Experimenting with Speculative UI language: A DSL for UI Components
I've been experimenting with this idea I'm calling spec-ui-lang—a YAML DSL for describing UI components. I'm curious if a structured text file could actually reduce ambiguity when asking AI to generate UI code. Let me try some simple examples and see what happens.
I've been experimenting with this idea I'm calling spec-ui-lang—a YAML DSL for describing UI components.
The premise is simple: if you ask an AI to "build me a button," you might get something that doesn't quite match what you had in mind. But if you give it a structured specification that says "this button should be 48px tall with 16px padding and this specific color," you're much more likely to get exactly what you want.
Or at least, that's my hypothesis.
I want to test whether this approach actually works in practice, so I wrote a complete reference document for spec-ui-lang, and now I'm going to try building some actual components from specs to see what happens.
The Basic Idea
spec-ui-lang uses YAML to describe UI components with five required sections:
- METADATA: Component identification and status
- ANATOMY: Hierarchical structure of parts
- DESIGN_TOKENS: Visual appearance (colors, typography, spacing)
- BEHAVIOR: States, interactions, and transitions
- RESPONSIVITY: Breakpoints and layout adaptation
For today, I'm keeping things relatively simple—I'll add some basic hover and focus states to the button to see how the BEHAVIOR section works, but no complex interactivity. Just components with clear structure, visual specifications, and a few well-defined states.
Demo 1: A Simple Button
Here's what a basic button spec looks like in spec-ui-lang:
METADATA:
Widget: SimpleButton
Standard: "spec-ui-lang"
Status: Draft-1.0.0
Version: 1.0.0
ANATOMY:
- Body:
Parts:
- Button:
Type: Action
Content: "Click Me"
DESIGN_TOKENS:
Surface: "#3B82F6"
SurfaceHover: "#2563EB"
SurfaceActive: "#1D4ED8"
SurfaceDisabled: "#4B5563"
CornerShape: "squircle"
CornerRadius: "8px"
FocusRing: "#60A5FA"
Typography:
Button:
FontSize: "16px"
FontWeight: "600"
Color: "#FFFFFF"
BEHAVIOR:
States:
- Hover:
Surface: "token:SurfaceHover"
Transition: "150ms ease-in-out"
- Focus:
Outline: "2px solid token:FocusRing"
OutlineOffset: "2px"
- Active:
Surface: "token:SurfaceActive"
Transform: "scale(0.98)"
- Disabled:
Surface: "token:SurfaceDisabled"
Opacity: "0.5"
Cursor: "not-allowed"
RESPONSIVITY:
Viewport: {}
From this spec, I'd generate React code like this:
<button className="bg-blue-500 [corner-shape:squircle] rounded-[8px] text-base font-semibold px-4 py-2 text-white transition-all duration-150 hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 active:bg-blue-700 active:scale-95 disabled:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed">
Click Me
</button>
And here's the button rendered in your browser:
Notice how the ANATOMY section defines a single Action part, and DESIGN_TOKENS specify the surface color (#3B82F6 maps to blue-500), corner radius, and corner shape. The CornerShape: "squircle" value maps to the corner-shape:squircle Tailwind utility class. In a real project, this would be a custom utility that creates smooth superellipse corners using CSS border-radius.
The BEHAVIOR section is where things get interesting. I defined four states—hover, focus, active, and disabled—each referencing design tokens using the token: prefix. For example, Hover uses token:SurfaceHover instead of a hardcoded color, making it easy to update all hover colors in one place. The Active state adds a scale transform to create a pressed effect, and Focus adds an outline using token:FocusRing for keyboard accessibility. The generated React component maps each state to its corresponding Tailwind utility classes with a 150ms transition for smooth state changes.
Demo 2: A Card Component
Let me try something slightly more complex—a card with a title and description:
METADATA:
Widget: SimpleCard
Standard: "spec-ui-lang"
Status: Draft-1.0.0
Version: 1.0.0
ANATOMY:
- Body:
Parts:
- Title:
Type: Text
Content: "Hello, World!"
- Description:
Type: Text
Content: "This is a card component."
DESIGN_TOKENS:
Surface: "#1F2937"
CornerShape: "squircle"
CornerRadius: "12px"
Border: "1px solid #374151"
Shadow: "0 1px 3px rgba(0,0,0,0.3)"
Gap: "12px"
Padding:
Top: "24px"
Right: "24px"
Bottom: "24px"
Left: "24px"
Typography:
Title:
FontSize: "20px"
FontWeight: "700"
Color: "#F9FAFB"
Description:
FontSize: "16px"
FontFamily: "system-ui, sans-serif"
Color: "#F9FAFB"
BEHAVIOR:
States: {}
RESPONSIVITY:
Viewport: {}
This spec defines two Text parts in the Body section, along with more detailed design tokens for padding, borders, and shadows.
Here's the generated React code:
<div className="bg-gray-800 [corner-shape:squircle] rounded-[12px] border border-gray-700 p-6 shadow-sm text-gray-50">
<h3 className="text-xl font-bold mb-3">Hello, World!</h3>
<p className="text-base">This is a card component.</p>
</div>
And here's the card:
Hello, World!
This is a card component.
I like how the Gap token in the spec translates to spacing between the title and description (mb-3). The spec makes it clear that there should be 12px between these elements without needing to specify the exact CSS class.
Demo 3: An Input Field
Let me try one more—an input field with a placeholder:
METADATA:
Widget: SimpleInput
Standard: "spec-ui-lang"
Status: Draft-1.0.0
Version: 1.0.0
ANATOMY:
- Body:
Parts:
- Input:
Type: Input
Placeholder: "Type something..."
DESIGN_TOKENS:
Surface: "#1F2937"
CornerShape: "squircle"
CornerRadius: "8px"
Border: "1px solid #374151"
Padding:
Top: "12px"
Right: "12px"
Bottom: "12px"
Left: "12px"
Typography:
Input:
FontSize: "16px"
Color: "#F9FAFB"
BEHAVIOR:
States: {}
RESPONSIVITY:
Viewport: {}
Generated React code:
<input
type="text"
placeholder="Type something..."
className="bg-gray-800 [corner-shape:squircle] rounded-[8px] border border-gray-700 px-3 py-3 text-base text-gray-50 placeholder:text-gray-500"
/>
And here's the input:
What I Learned So Far
From these simple demos, I'm seeing a few things I like about this approach:
-
Explicit is better than implicit: The spec forces me to think through every aspect of the component upfront. I can't be vague about spacing, colors, or typography.
-
Language-agnostic structure: The YAML doesn't care whether I'm generating React, SwiftUI, or Flutter code. I could take the same button spec and generate Swift instead.
-
Design-system friendly: spec-ui-lang lets you define design tokens in
DESIGN_TOKENSand reference them inBEHAVIORusing thetoken:prefix. The button spec usestoken:SurfaceHover,token:SurfaceActive, andtoken:FocusRinginstead of hardcoded colors, making it easy to maintain consistent theming across all component states. -
AI-friendly structure: When I ask an AI to "generate React code from this spec-ui-lang spec," there's very little room for ambiguity. Every decision point is spelled out.
What I'm Still Unsure About
I'm curious to see whether this approach scales. These are relatively simple components—though the button does demonstrate state management. What happens when I need to describe:
- More complex animations and transitions?
- Component composition (a form built from inputs, buttons, labels)?
- Responsive behavior that changes layout on different screen sizes?
These are the things I'll explore in the next few posts. I want to know: does spec-ui-lang actually make it easier to build real, production components, or is it just overkill for simple things?
Coming Up Next
In the next post, I'll build a more complex component that combines multiple parts. I want to see whether spec-ui-lang can effectively describe a counter widget with increment/decrement buttons—but importantly, the spec will describe the component's structure and visual states, while the business logic (when to disable buttons based on min/max bounds) will be handled by the app code.
This distinction matters because spec-ui-lang is designed to describe static, stateless components that are manipulated via props. The state and business rules live in the application layer, not in the UI spec. I'm curious to see how this separation plays out in practice when describing interactive components.