ANTIGRAVITY LABJP
Articles/AI Tools
AI Tools/2026-03-27Intermediate

Why AI-Generated UI Feels Off — Beyond Surface-Level Design Quirks to the Core Problem of Missing Intent

The uncanny feeling when looking at AI-generated UI isn't just about gradients and emoji overuse. The real issue runs deeper: the absence of human judgment embedded in design decisions. Learn how type systems, design tokens, and conditional logic can carry intentionality.

ai-tools14ui-design2design-judgmenttailwind2typescript26vibe-coding5

When you spend time examining AI-generated UI, there's often an unsettling feeling: something's off.

The suspects are obvious—overly enthusiastic gradients, scattered emoji, excessive border-radius, shadow layering that feels arbitrary. These surface features certainly catch the eye. But they're not the real problem. The fundamental reason AI-generated UI carries a sense of artificiality is the absence of human judgment baked into every design decision.

Throughout Antigravity Lab, we've championed "vibe coding" as a philosophy—which at its core means injecting judgment into AI workflows. This article explores how that judgment manifests at the component implementation level: how to make UI code that doesn't just look human-designed, but reasons like one.

The Surface Symptoms of AI-Generated UI

Let's start with what our eyes register.

The "Safe" Gradient

from-indigo-500 to-purple-600. You've seen this a thousand times in AI output. It's a statistically low-risk color combination. Since AI trains on successful designs, it gravitates toward color pairings that rarely offend. Nothing fails, but nothing stands out either.

The problem isn't the gradient itself. It's that there's no reasoning behind it. Maybe your brand primary is darker. Maybe you want a monochrome-first approach with color only for CTAs. The AI simply cannot ask, "Why?"

Emoji Proliferation

🚀 🎉 ✨ 💡 🔥

AI treats emoji as a quantifiable variable in "approachability." More emoji = friendlier = better. But emoji placement and density require human judgment about your audience. Will this resonate, or will it feel hollow to your users?

Border-Radius Excess

rounded-2xl and rounded-full seem risk-free—modern, friendly, approachable. That's exactly why AI favors them. It learned they're statistically safe.

But is every button truly better as rounded-2xl? For touch targets, what matters most isn't border-radius; it's that the interactive area meets the 44px minimum for accessible finger tapping.

The Core Issue: Intentionality in Code

This is where we shift from observation to essence.

What separates thoughtfully designed UI from AI-generated UI? Human intent embedded in decisions.

When a CTA button has higher contrast than surrounding elements, it's because the designer judged that particular action as most important to the user's goal.

When error buttons have subtle border-radius, it's because there's deliberate restraint—"this is a dangerous action, give it visual weight and force the user to look carefully."

When a modal darkens the background, it's a conscious choice to eliminate distraction and focus attention.

AI has no such chain of intentional reasoning. It selects statistically "good" elements and combines them. The "why" is missing.

Three Ways to Embed Judgment in Code

Let's get practical. How do we encode human judgment into our components?

1. Express Intent Through Types

Consider a naive Button component:

// No judgment here
interface ButtonProps {
  color?: string; // 'blue' | 'red' | 'green' ...
  children: React.ReactNode;
}
 
export function Button({ color = 'blue', children }: ButtonProps) {
  return (
    <button className={`bg-${color}-500 text-white px-4 py-2 rounded-lg`}>
      {children}
    </button>
  );
}

This is judgment-free. <Button color="red">Delete</Button> creates a red button, but the "why" is opaque.

Now, let's encode intent into the type signature:

// Intent encoded in types
type ButtonVariant = 'primary' | 'secondary' | 'destructive' | 'ghost' | 'link';
type ButtonSize = 'sm' | 'md' | 'lg';
 
interface ButtonProps {
  variant?: ButtonVariant; // Meaningful choices only
  size?: ButtonSize;
  isLoading?: boolean;
  disabled?: boolean;
  children: React.ReactNode;
}
 
export function Button({
  variant = 'primary',
  size = 'md',
  isLoading = false,
  disabled = false,
  children
}: ButtonProps) {
  const baseStyles = 'font-medium transition-all duration-200 focus:ring-2';
 
  const variantStyles = {
    primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-300',
    secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-400',
    destructive: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-300',
    ghost: 'bg-transparent text-gray-900 hover:bg-gray-100 focus:ring-gray-300',
    link: 'bg-transparent text-blue-600 underline hover:no-underline focus:ring-blue-300',
  };
 
  const sizeStyles = {
    sm: 'px-3 py-1.5 text-sm',
    md: 'px-4 py-2 text-base min-h-[44px]', // Accessible touch target
    lg: 'px-6 py-3 text-lg min-h-[48px]',
  };
 
  return (
    <button
      className={`${baseStyles} ${variantStyles[variant]} ${sizeStyles[size]} ${
        isLoading ? 'opacity-70 cursor-wait pointer-events-none' : ''
      } ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
      disabled={disabled || isLoading}
    >
      {isLoading ? 'Processing...' : children}
    </button>
  );
}

What changed:

  • Variant as intent: "This is the primary action," "This is destructive," "This is supplementary"—all encoded in a constrained type
  • 44px minimum in size: Accessibility judgment baked into the implementation
  • isLoading behavior unified: When loading, the component exhibits a coherent behavior (wait cursor, reduced opacity, disabled state) that reflects a judgment about UX

2. Embed Brand Judgment in Design Tokens

Now let's see how design tokens carry intentionality:

// tailwind.config.ts
export default {
  theme: {
    colors: {
      // Brand palette reflecting Antigravity's world
      surface: 'oklch(0.98 0.002 0)',      // Slightly warm white, not pure white
      'surface-alt': 'oklch(0.94 0.002 0)',
 
      // Semantic colors with clear roles
      primary: 'oklch(0.55 0.2 260)',      // Brand primary (blue-violet)
      secondary: 'oklch(0.65 0.1 260)',    // Brand secondary (lighter)
 
      success: 'oklch(0.58 0.15 142)',     // Intent: confirmation
      warning: 'oklch(0.68 0.15 65)',      // Intent: caution
      error: 'oklch(0.55 0.2 25)',         // Intent: danger
      info: 'oklch(0.60 0.15 250)',        // Intent: information
    },
    spacing: {
      // Deliberate scale based on Fibonacci proportions
      'xs': '0.25rem',   // Text-level whitespace
      'sm': '0.5rem',    // Tight element spacing
      'md': '1rem',      // Standard spacing
      'lg': '1.5rem',    // Section spacing
      'xl': '2.5rem',    // Major section gaps
      'xxl': '4rem',     // Page margins
    },
    fontSize: {
      // Readability judgment: size paired with line-height
      'xs': ['0.75rem', { lineHeight: '1.5' }],
      'sm': ['0.875rem', { lineHeight: '1.5' }],
      'base': ['1rem', { lineHeight: '1.6' }],
      'lg': ['1.125rem', { lineHeight: '1.6' }],
      'xl': ['1.375rem', { lineHeight: '1.5' }],
      'h2': ['1.75rem', { lineHeight: '1.3' }],
      'h1': ['2.25rem', { lineHeight: '1.2' }],
    },
  },
};

Notice:

  • oklch color space: Closer to human perception than RGB or HSL. More intentional brand presence
  • Semantic color names: primary, error, success—the role is clear
  • Paired line-heights: Headings get tighter spacing (1.2), body text gets room to breathe (1.6)

3. Encode "Why" in Conditional Logic

Finally, conditional branches themselves are expressions of judgment:

// FormInput.tsx - Accessibility judgment baked into conditions
interface FormInputProps {
  label: string;
  value: string;
  onChange: (val: string) => void;
  error?: string;
  isLoading?: boolean;
  required?: boolean;
}
 
export function FormInput({
  label,
  value,
  onChange,
  error,
  isLoading,
  required,
}: FormInputProps) {
  const inputId = `input-${label.replace(/\s/g, '-')}`;
 
  return (
    <div className="flex flex-col gap-md">
      <label htmlFor={inputId} className="font-medium text-gray-900">
        {label}
        {required && <span className="text-error ml-1">*</span>}
      </label>
 
      <input
        id={inputId}
        type="text"
        value={value}
        onChange={(e) => onChange(e.target.value)}
        disabled={isLoading}
        // Error state judgment:
        // - Visual distinction (red border)
        // - Screen reader users notified (aria-invalid)
        // - Error message linked via aria-describedby
        aria-invalid={!!error}
        aria-describedby={error ? `${inputId}-error` : undefined}
        className={`
          px-3 py-2 border-2 rounded-md transition-colors
          ${error
            ? 'border-error bg-error/5 focus:ring-error/30'
            : 'border-gray-300 focus:ring-primary/30'
          }
          ${isLoading ? 'cursor-wait opacity-70' : ''}
          focus:outline-none focus:ring-2
        `}
      />
 
      {/* Error display: role="alert" for immediate announcement */}
      {error && (
        <div
          id={`${inputId}-error`}
          role="alert"
          aria-live="polite"
          className="text-error text-sm font-medium"
        >
          {error}
        </div>
      )}
    </div>
  );
}

The judgments encoded here:

  • aria-invalid + aria-describedby: We judge it important that screen reader users receive exact error information
  • role="alert" + aria-live="polite": We judge that error feedback should interrupt and be announced immediately
  • cursor-wait: We judge that showing "the system is thinking" provides psychological reassurance

Why This Matters

All three approaches share one trait: every choice has a "why."

Button variants exist because user psychology benefits from consistent, limited choices. The 44px minimum exists because elderly users and those with larger hands deserve full accessibility. aria-live attributes exist because blind users deserve the same real-time feedback as sighted users.

AI feels "off" because this interlocking chain of intentional reasoning is absent. Conversely, code that encodes such reasoning naturally feels more human—because it was designed by humans thinking clearly.

In Vibe Coding Practice

Antigravity Lab's "vibe coding" methodology is precisely about injecting this judgment into AI generation. Here's an example:

# Prompt: Pass design tokens first
 
Reference the tailwind.config.ts above.
 
Implement a Button component with:
- variant: 'primary' | 'secondary' | 'destructive' | 'ghost' | 'link'
- size: 'sm' | 'md' | 'lg' (minimum touch target 44px)
- isLoading state: cursor-wait, opacity-70, disabled
- Full role and aria-* attributes for assistive tech
- Use ONLY colors, spacing, typography from design tokens
- For each conditional branch, add a comment explaining the UX reasoning

When you provide AI with explicit tokens and reasoning before generation, the output quality transforms.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

AI Tools2026-03-27
Design Judgment in Code: Beyond AI-Generated UI — Practical Techniques for Embedding Intent into Types, Tokens, and Conditionals
Master the comprehensive techniques for embedding human design judgment into code. From TypeScript's type system for expressing UI intent, through strategic design token architecture, to semantic conditional branching and component API patterns. Learn how to inject judgment into AI-generated code and build design systems that reason like humans.
AI Tools2026-06-24
When an Overnight Local Agent Crawls by Dawn — Keeping Ollama's Latency Flat by Working Backward from Context Length
Why each step of a long-running local agent gets heavier toward the end, how to measure it from Ollama's timing fields, and how a fixed num_ctx plus a rolling summary keep per-step latency flat.
AI Tools2026-06-14
Pairing a Local LLM With Antigravity to Keep Sensitive Code Off the Cloud
Should you really let a cloud agent read code that holds your billing keys and revenue logic? For indie developers that worry is concrete. Here I pair Ollama and Gemma as a local LLM with Antigravity, routing sensitive parts to local and general parts to the cloud, with the decision rules and measurements.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →