Jede Rails-App ab einer gewissen Grösse leidet unter dem gleichen Problem: Die UI wird inkonsistent. Buttons sehen auf jeder Seite anders aus, Formulare verhalten sich unterschiedlich, und niemand weiss mehr, welches Partial die “richtige” Version ist. Ein Design-System löst das, aber in Rails gibt es mehrere Wege dorthin. Dieser Artikel zeigt, welcher Ansatz in der Praxis funktioniert.

Warum Partials für Design-Systeme nicht reichen

Rails-Partials sind der Standardweg für wiederverwendbare UI-Elemente. Für einfache Projekte funktioniert das. Sobald eine App aber 50+ Views hat, zeigen sich die Schwächen:

  • Kein Interface-Vertrag: Partials akzeptieren beliebige Locals ohne Validierung. Ein fehlendes locals-Argument fällt erst zur Laufzeit auf.
  • Kein isolierter Test: Partials lassen sich nur über Integration Tests prüfen. Unit Tests für einzelne UI-Elemente sind nicht vorgesehen.
  • Implicit Dependencies: Partials greifen oft auf Instanzvariablen zu, die der Controller setzt. Das macht Refactoring riskant.
  • Keine Preview-Möglichkeit: Ohne den passenden Controller-Context lässt sich ein Partial nicht isoliert betrachten.

Diese Probleme wachsen mit der Teamgrösse. In einer App mit 5+ Entwicklern führt das schnell zu doppelten Implementierungen und inkonsistenter UI.

ViewComponent als Basis: Das steckt dahinter

ViewComponent von GitHub löst die zentralen Partial-Probleme durch echte Ruby-Objekte mit klarem Interface:

# app/components/button_component.rb
class ButtonComponent < ViewComponent::Base
  VARIANTS = %i[primary secondary danger ghost].freeze
  SIZES = %i[sm md lg].freeze

  erb_template <<~ERB
    <button class="<%= classes %>" <%= html_attributes %>>
      <%= content %>
    </button>
  ERB

  def initialize(variant: :primary, size: :md, **html_attrs)
    raise ArgumentError, "Unknown variant: #{variant}" unless VARIANTS.include?(variant)
    raise ArgumentError, "Unknown size: #{size}" unless SIZES.include?(size)

    @variant = variant
    @size = size
    @html_attrs = html_attrs
  end

  private

  def classes
    "btn btn--#{@variant} btn--#{@size}"
  end

  def html_attributes
    @html_attrs.map { |k, v| "#{k}=\"#{v}\"" }.join(" ").html_safe
  end
end

Der entscheidende Unterschied zu Partials:

EigenschaftPartialViewComponent
TypsicherheitKeine (beliebige Locals)Ruby-Konstruktor mit Validierung
Unit TestsNicht vorgesehenrender_inline im Test
PreviewNur via ControllerLookbook / ViewComponent::Preview
Performance1x (Baseline)~10x schneller (GitHub-Benchmark)
Slot-SystemNicht vorhandenContent Slots für komplexe Layouts

GitHub selbst hat 2023 über 200 UI-Komponenten auf ViewComponent migriert und berichtet von signifikant weniger UI-Inkonsistenzen.

Lookbook: Der fehlende Style Guide für Rails

Ein Design-System ohne visuelle Dokumentation ist nur eine Komponentenbibliothek. Lookbook schliesst diese Lücke für Rails.

Lookbook generiert automatisch einen interaktiven Style Guide aus ViewComponent-Previews:

# test/components/previews/button_component_preview.rb
class ButtonComponentPreview < Lookbook::Preview
  # @param variant select { choices: [primary, secondary, danger, ghost] }
  # @param size select { choices: [sm, md, lg] }
  def default(variant: :primary, size: :md)
    render ButtonComponent.new(variant: variant.to_sym, size: size.to_sym) do
      "Klick mich"
    end
  end

  def all_variants
    render_with_template
  end
end

Damit erhalten Designer und Entwickler eine gemeinsame Referenz unter /lookbook im Development-Modus. Jede Komponente ist dort interaktiv testbar, mit Parametern, die sich live anpassen lassen.

Setup in 3 Minuten:

bundle add lookbook
# config/routes.rb (nur development)
mount Lookbook::Engine, at: "/lookbook" if Rails.env.development?

Für Teams ab 3 Personen ist Lookbook kein Nice-to-have, sondern spart messbar Zeit bei Design-Reviews und Onboarding.

Rails 8 + KI + Tailwind Das Ultimative Designsystem-Tutorial 2024

Tailwind CSS oder eigene Design Tokens?

Die Frage “Tailwind oder Custom CSS?” stellt sich in jedem Rails-Projekt. Unsere Erfahrung nach über 15 Jahren Rails-Entwicklung:

Tailwind funktioniert gut, wenn:

  • Das Team >3 Personen hat und einheitliches Styling braucht
  • Schnelle Prototypen gefragt sind
  • Die App kein stark individuelles visuelles Design benötigt

Custom Design Tokens sind besser, wenn:

  • Das Branding komplex ist (mehrere Produkte, White-Label)
  • Performance kritisch ist (Tailwind’s Utility-Klassen erzeugen grössere HTML-Dateien)
  • Das Team CSS-Expertise hat

In der Praxis kombinieren wir bei USEO oft beides: Tailwind für Layout und Spacing, Custom Properties für Farben und Typografie.

// tailwind.config.js - Design Tokens als Tailwind-Theme
module.exports = {
  content: [
    './app/views/**/*.html.erb',
    './app/components/**/*.{rb,html.erb}',
    './app/helpers/**/*.rb'
  ],
  theme: {
    extend: {
      colors: {
        brand: {
          DEFAULT: 'var(--color-brand)',
          light: 'var(--color-brand-light)',
          dark: 'var(--color-brand-dark)',
        }
      },
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif']
      }
    }
  }
}

So bleiben die Tailwind-Utility-Klassen konsistent, aber die konkreten Farbwerte lassen sich zentral via CSS Custom Properties steuern.

USEO’s Take: Unser Design-System-Ansatz in Rails

Nach hunderten Rails-Projekten haben wir bei USEO einen klaren Ansatz entwickelt:

1. ViewComponent ab Tag 1

Wir starten jedes neue Projekt mit ViewComponent. Nicht weil Partials schlecht sind, sondern weil der Wechsel später teuer wird. Eine Migration von 80+ Partials zu ViewComponent hat uns in einem Kundenprojekt ca. 3 Wochen gekostet. Der gleiche Aufwand am Anfang: 2 Tage Setup.

2. Lookbook als lebende Dokumentation

Figma-Dateien veralten. Lookbook nicht, weil es direkt aus dem Code generiert wird. Wir nutzen es als Single Source of Truth für UI-Entscheidungen.

3. Komponentenhierarchie mit Atomic Design

Wir strukturieren ViewComponents in drei Ebenen:

app/components/
├── atoms/          # ButtonComponent, BadgeComponent, IconComponent
├── molecules/      # FormFieldComponent, CardComponent, NavItemComponent
└── organisms/      # HeaderComponent, SidebarComponent, DataTableComponent

Atome haben keine Abhängigkeiten auf andere Komponenten. Moleküle kombinieren Atome. Organismen kombinieren Moleküle. Diese Regel wird im Code Review durchgesetzt.

4. Visuelle Regressionstests mit Percy oder Capybara-Screenshot

# spec/components/button_component_spec.rb
RSpec.describe ButtonComponent, type: :component do
  it "renders primary variant" do
    render_inline(described_class.new(variant: :primary)) { "Submit" }

    expect(page).to have_css("button.btn.btn--primary")
    expect(page).to have_text("Submit")
  end

  it "rejects invalid variants" do
    expect {
      render_inline(described_class.new(variant: :invalid)) { "Test" }
    }.to raise_error(ArgumentError)
  end
end

Unit Tests für jede Komponente sind Pflicht. Dazu kommen visuelle Snapshot-Tests, die Layout-Regressionen automatisch erkennen.

Hotwire und Turbo: Interaktivität ohne Chaos

Ein Design-System muss auch dynamische UI abdecken. Hotwire mit Turbo Frames und Turbo Streams integriert sich nahtlos mit ViewComponent:

<%# app/views/tasks/index.html.erb %>
<%= turbo_frame_tag "task_list" do %>
  <% @tasks.each do |task| %>
    <%= render TaskCardComponent.new(task: task) %>
  <% end %>
<% end %>

Turbo Streams aktualisieren einzelne Komponenten, ohne die gesamte Seite neu zu laden. Die UI-Logik bleibt dabei serverseitig in den ViewComponents, statt in JavaScript-Fragmenten verstreut zu sein.

Der Vorteil gegenüber React oder Vue: Kein zweites Komponentensystem. Die gleichen ViewComponents rendern sowohl die initiale Seite als auch dynamische Updates.

Legacy-Rails-App modernisieren: Schritt für Schritt

Die meisten Rails-Apps haben kein Design-System, sondern gewachsenes CSS und hunderte Partials. Eine Migration auf ViewComponent funktioniert am besten inkrementell:

Phase 1: Audit (1-2 Tage)

Inventar der bestehenden UI-Elemente erstellen. Häufig finden sich 5+ Varianten des gleichen Buttons.

Phase 2: Foundation (1 Woche)

Design Tokens definieren, ViewComponent und Lookbook einrichten, 5-10 Basis-Komponenten bauen (Button, Input, Card, Badge, Alert).

Phase 3: Migration (laufend)

Bei jedem Feature oder Bugfix die betroffenen Partials durch ViewComponents ersetzen. Kein Big Bang, sondern organische Migration.

Phase 4: Enforcement

Linter-Regel einführen, die neue Partials in Verzeichnissen flaggt, wo bereits ViewComponents existieren. RuboCop Custom Cops eignen sich dafür.

Bei USEO haben wir diesen Prozess für mehrere Kunden durchgeführt, auch für Schweizer Unternehmen mit mehrsprachigen Anforderungen (DE, FR, IT). Die Ergebnisse sind konsistent: weniger UI-Bugs, schnelleres Onboarding neuer Entwickler, und messbar kürzere Feature-Zyklen.

Zugänglichkeit von Anfang an einbauen

Ein Design-System ist der ideale Ort, um Barrierefreiheit zentral zu verankern. Statt ARIA-Attribute in jedem View einzeln zu setzen, werden sie Teil der Komponente:

class AlertComponent < ViewComponent::Base
  ROLES = { info: "status", warning: "alert", error: "alert" }.freeze

  def initialize(type: :info)
    @type = type
  end

  def role
    ROLES.fetch(@type)
  end

  def aria_live
    @type == :error ? "assertive" : "polite"
  end
end
<div role="<%= role %>" aria-live="<%= aria_live %>" class="alert alert--<%= @type %>">
  <%= content %>
</div>

Zugänglichkeit wird so zum Default, nicht zur Ausnahme. Neue Teammitglieder können keine unzugänglichen Alerts bauen, weil die Komponente es nicht zulässt.

Toolchain-Übersicht

ToolZweckGem / PackageEmpfehlung
ViewComponentUI-Komponenten mit Ruby-Interfaceview_componentPflicht
LookbookInteraktiver Style GuidelookbookPflicht ab 3+ Devs
Tailwind CSSUtility-first Stylingtailwindcss-railsEmpfohlen
Primer ViewComponentsFertige, zugängliche Komponentenprimer_view_componentsOptional (GitHub-spezifisch)
PercyVisuelle RegressionstestsPercy CIEmpfohlen für grössere Apps
RuboCopLinting für KomponentenregelnCustom CopsEmpfohlen

Fazit

Design-Systeme in Rails sind kein abstraktes Konzept. Mit ViewComponent, Lookbook und Tailwind gibt es ein ausgereiftes Tooling, das sich in der Praxis bewährt hat. Der wichtigste Schritt: Früh anfangen. Je länger eine App ohne Design-System wächst, desto teurer wird die Migration.

Wer eine bestehende Rails-App modernisieren will oder ein neues Projekt von Anfang an richtig aufsetzen möchte, kann sich an USEO wenden. Wir sind auf Ruby on Rails spezialisiert und haben diesen Prozess dutzende Male durchgeführt.

FAQs

Wann lohnt sich der Umstieg von Partials auf ViewComponent?

Ab dem Moment, wo mehrere Entwickler an der gleichen App arbeiten oder die Zahl der Partials unübersichtlich wird (typisch ab 30-50 Partials). Der Aufwand für die initiale Einrichtung ist gering (1-2 Tage), die langfristigen Vorteile durch testbare, typisierte Komponenten aber erheblich.

Ist Primer ViewComponents für Nicht-GitHub-Projekte sinnvoll?

Bedingt. Primer bringt GitHubs Design-Sprache mit, was für interne Tools gut passt. Für kundenspezifische Produkte mit eigenem Branding empfehlen wir eigene ViewComponents mit Tailwind. So behalten Sie volle Kontrolle über das visuelle Design.

Wie halte ich das Design-System aktuell?

Drei Massnahmen: (1) Lookbook als Single Source of Truth, (2) visuelle Regressionstests im CI, (3) Code-Review-Regel, dass neue UI-Elemente als ViewComponent gebaut werden müssen. Ohne diese Disziplin veraltet jedes Design-System innerhalb von 6 Monaten.

Verwandte Artikel