Zurück zu den Artikeln

Crafting a Modern UI with Tailwind and Catalyst

How I combined Tailwind CSS v3 with Tailwind UI's Catalyst components to create a professional admin interface, all adapted for Phoenix LiveView—with some AI assistance.

Crafting a Modern UI with Tailwind and Catalyst

With Phoenix as my foundation, the next challenge was creating a beautiful, professional interface. I needed something that looked polished out of the box but remained fully customizable. Enter Tailwind CSS and Catalyst UI Kit—and my secret weapon: GitHub Copilot with Claude Sonnet 4.5.

Why Tailwind CSS?

After years of fighting with CSS frameworks like Bootstrap and custom SCSS architectures, Tailwind’s utility-first approach felt like a revelation:

Utility-First Philosophy

Instead of writing semantic class names and custom CSS:

<!-- Old way -->

<div class="card card-primary">
  <h2 class="card-title">Title</h2>
</div>

Tailwind lets you compose designs directly in HTML:

<!-- Tailwind way -->

<div class="rounded-lg bg-white p-6 shadow-lg">
  <h2 class="text-2xl font-bold text-zinc-900">Title</h2>
</div>

Benefits I Experienced

1. No Context Switching

  • Write HTML and styles together
  • See the design taking shape in real-time
  • No jumping between files

2. Consistency by Default

  • Predefined spacing scale (4px, 8px, 12px, etc.)
  • Color system with multiple shades
  • Typography scale that just works
  • Responsive breakpoints built-in

3. Tiny Production Builds

  • PurgeCSS removes unused classes
  • My production CSS: ~12KB gzipped
  • No bloated framework CSS

4. Dark Mode Made Easy

<div class="bg-white text-zinc-900 dark:bg-zinc-900 dark:text-white">
  Automatically switches based on user preference
</div>

Discovering Catalyst UI Kit

While Tailwind handles utility classes beautifully, building an entire admin interface from scratch is time-consuming. I needed a component library that:

  • Looked professional and modern
  • Was built with Tailwind
  • Provided common UI patterns (forms, tables, modals)
  • Wasn’t too opinionated

Catalyst UI Kit from Tailwind Labs was perfect. It’s a commercial component library with:

  • Pre-built React components
  • Beautiful Tailwind-based styling
  • Accessible by default (using Headless UI)
  • Consistent design language

AI-Powered Development: GitHub Copilot + Claude Sonnet 4.5

Here’s where things get interesting. As an experienced software engineer, I’ve been around long enough to remember when we coded everything from scratch. But I’m also pragmatic enough to leverage the best tools available. Enter GitHub Copilot with Claude Sonnet 4.5.

Why This Matters

Let me be clear: AI didn’t write this application for me. What it did was act as an incredibly sophisticated pair programming partner, allowing me to:

  1. Move at thought speed - Express intent in natural language and get implementation suggestions
  2. Reference multiple codebases - Draw patterns from both Catalyst (React) and Spotlight (Next.js) templates
  3. Maintain consistency - Get component suggestions that matched my established patterns
  4. Catch issues early - Real-time feedback on Elixir/Phoenix idioms

The Human + AI Development Process

Day 1 (Feb 3, 2026) - Foundation

  • Morning: mix phx.new spotlight and Nix shell setup
  • Asked Copilot: “Convert Catalyst Button component to Phoenix.Component”
  • Reviewed suggestions, adapted for Phoenix patterns
  • Built initial admin component library by studying both codebases
  • My expertise: Knowing which patterns to adopt, which to modify

Day 2 (Feb 4, 2026) - Feature Development

  • Implemented media management with drag-drop uploads
  • Copilot suggested Mogrify integration for image optimization
  • I evaluated: security implications, performance trade-offs, deployment requirements
  • Built avatar picker modal - AI suggested patterns, I designed UX

What AI Did Well

Pattern Recognition: Suggested Phoenix Component syntax based on React examples

Boilerplate Generation: Created schema migrations, context functions

Code Translation: Converted Tailwind v4 patterns to v3 compatible syntax

Documentation: Generated comprehensive inline comments

What Required Human Expertise

🧠 Architecture Decisions: Database schema design, context boundaries

🧠 UX Design: Avatar picker modal flow, empty states, error handling

🧠 Security: CSRF protection, file upload validation, authentication patterns

🧠 Performance: Query optimization, image processing pipeline

🧠 Integration: Making Catalyst patterns work with LiveView semantics

The Result

In 2 days (Feb 3-4, 2026), I built:

  • Complete Phoenix application with LiveView
  • Admin dashboard with 8+ CRUD interfaces
  • Media management with image optimization
  • Multi-language support (German/English)
  • Dark mode throughout
  • Professional UI adapted from commercial templates

Could I have done this without AI? Yes. Would it have taken 2 days? Absolutely not. More like 2 weeks.

The key insight: AI is a force multiplier for experienced developers, not a replacement. You still need to:

  • Know what to ask for
  • Evaluate suggestions critically
  • Understand the underlying patterns
  • Make architectural decisions
  • Debug and refine

Adapting Catalyst to Phoenix LiveView

Back to the technical details. Catalyst is built for React, but I needed it for Phoenix. The solution? Rewrite components as Phoenix.Component function components.

The Conversion Process

1. Study the React Component Catalyst’s Button component in TypeScript:

// Catalyst Button (simplified)
export function Button({ variant = 'solid', color = 'zinc', children, ...props }) {
  const styles = {
    solid: 'bg-zinc-900 text-white hover:bg-zinc-800',
    outline: 'border border-zinc-300 hover:bg-zinc-50'
  }
  return (
    <button className={styles[variant]} {...props}>
      {children}
    </button>
  )
}

2. Convert to Phoenix Component

# Phoenix Component version
attr :variant, :string, default: "solid", values: ~w(solid outline plain)
attr :color, :string, default: "zinc"
attr :rest, :global
slot :inner_block, required: true

def admin_button(assigns) do
  ~H"""
  <button
    class={[
      "rounded-lg px-4 py-2 text-sm font-semibold transition",
      @variant == "solid" && "bg-zinc-900 text-white hover:bg-zinc-800",
      @variant == "outline" && "border border-zinc-300 hover:bg-zinc-50"
    ]}
    {@rest}
  >
    {render_slot(@inner_block)}
  </button>
  """
end

3. Add LiveView Enhancements

# Add navigation support for LiveView
attr :patch, :string, default: nil
attr :navigate, :string, default: nil
attr :href, :string, default: nil

def admin_button(assigns) do
  ~H"""
  <.link
    :if={@patch || @navigate || @href}
    patch={@patch}
    navigate={@navigate}
    href={@href}
    class="..."
  >
    {render_slot(@inner_block)}
  </.link>
  <button :if={!@patch && !@navigate && !@href} class="..." {@rest}>
    {render_slot(@inner_block)}
  </button>
  """
end

Components I Built

I converted and adapted dozens of Catalyst components:

Layout Components:

  • admin_page_header - Page titles with action buttons
  • admin_form_container - Consistent form wrappers
  • admin_card - Content containers with optional sections

Form Elements:

  • admin_button - Multiple variants and colors
  • input - Extended core component with better styling
  • admin_badge - Status indicators with 17 color options

Data Display:

  • admin_list_item - Card-based list rows
  • admin_description_list - Key-value pairs
  • admin_empty_state - Helpful empty screens

Each component supports:

  • Dark mode out of the box
  • Consistent spacing and typography
  • Accessibility features
  • Phoenix LiveView events

Tailwind Configuration for Phoenix

Getting Tailwind working with Phoenix required some configuration:

// assets/tailwind.config.js
module.exports = {
  content: [
    './js/**/*.js',
    '../lib/*_web.ex',
    '../lib/*_web/**/*.*ex'
  ],
  darkMode: 'class', // Enable manual dark mode
  theme: {
    extend: {
      // Custom colors, fonts, etc.
    }
  },
  plugins: [
    require('@tailwindcss/forms'),
    require('@tailwindcss/typography')
  ]
}

I also added custom variants for LiveView loading states:

// Custom variants for phx loading states
plugin(({ addVariant }) => {
  addVariant('phx-click-loading', ['.phx-click-loading&'])
  addVariant('phx-submit-loading', ['.phx-submit-loading&'])
  addVariant('phx-change-loading', ['.phx-change-loading&'])
})

Now I can show loading states easily:

<button class="phx-submit-loading:opacity-50" phx-click="save">
  Save
</button>

Dark Mode Implementation

I implemented dark mode using Alpine.js for state management:

<html x-data="{ theme: localStorage.getItem('theme') || 'dark' }">
  <body x-bind:class="theme">
    <button @click="theme = theme === 'dark' ? 'light' : 'dark';
                    localStorage.setItem('theme', theme)">
      Toggle Theme
    </button>
  </body>
</html>

Every component includes dark mode variants:

<div class="bg-white text-zinc-900 dark:bg-zinc-900 dark:text-white">
  Content adapts automatically
</div>

Results

The combination of Tailwind and adapted Catalyst components gave me:

Professional Look: Admin interface looks as good as commercial SaaS products

Consistent Design: Every component follows the same patterns

Fast Development: Building new pages takes minutes, not hours

Tiny Bundle Size: Only 12KB of CSS in production

Fully Responsive: Mobile-first design works everywhere

Dark Mode: Seamless theme switching

Accessible: ARIA attributes and keyboard navigation

Lessons Learned

  1. Utility-first CSS is liberating: Once you adapt to Tailwind’s approach, traditional CSS feels cumbersome
  2. Component libraries save time: Don’t reinvent common patterns
  3. Adapt, don’t adopt: Converting React components to Phoenix gave me full control
  4. Dark mode should be first-class: Build it in from the start, not as an afterthought
  5. Design systems matter: Consistent spacing and colors make everything look professional

In my next article, I’ll explore the current state of my design and what features I’ve implemented.

Next up: My Current Design State: Features and Learnings