How Maybe's Tailwind CSS Design System Is Organized with Functional Tokens

The Maybe app uses a single-source-of-truth design system where functional tokens—defined via the @utility directive—map directly to CSS custom properties and automatically handle light/dark mode switching through centralized theme variants.

The maybe-finance/maybe repository implements a modular Tailwind CSS architecture that separates design intent from markup implementation. Rather than scattering arbitrary utility classes across ViewComponents and ERB templates, the codebase relies on functional tokens—semantic utility classes that abstract color, spacing, and component states. This approach ensures consistent theming across the entire Rails application while simplifying maintenance.

Central Entry Point

All design system tokens flow through a single entry point located at app/assets/tailwind/maybe-design-system.css. This file acts as the hub that imports specialized token groups and declares global CSS variables.

The import structure organizes utilities by function:

@import './maybe-design-system/background-utils.css';
@import './maybe-design-system/foreground-utils.css';
@import './maybe-design-system/text-utils.css';
@import './maybe-design-system/border-utils.css';
@import './maybe-design-system/component-utils.css';

This modular structure—found at lines 8–12 of maybe-design-system.css—allows developers to locate specific token definitions quickly without navigating monolithic stylesheets.

Core Color Variables

The same entry file contains a @theme block that declares CSS custom properties for every hue used across the UI. These variables define the raw color palette—grays, reds, greens, blues—and semantic colors like --color-success or --color-shadow.

Located at lines 21–45 in maybe-design-system.css, this @theme declaration serves as the single source of truth that all functional tokens ultimately reference. By centralizing raw values here, the system ensures that changing a color in one place propagates through every utility class.

Functional Token Groups

The architecture divides tokens into discrete categories based on their application context. Each group uses the @utility directive—a Tailwind CSS v4 feature—to generate first-class utility classes.

Background Utilities

The background-utils.css file defines surface and container colors through the bg-* namespace. Each utility applies a light-mode color by default and overrides it via the @variant theme-dark block when the user selects dark mode.

For example, the bg-surface token applies bg-gray-50 in light mode and switches to bg-black when the theme changes:

@utility bg-surface {
  @apply bg-gray-50;
  @variant theme-dark {
    @apply bg-black;
  }
}

This definition appears at lines 1–6 of app/assets/tailwind/maybe-design-system/background-utils.css.

Foreground Utilities

Separate from text colors, foreground utilities handle icons and inner elements that need to contrast against their parent backgrounds. Defined in foreground-utils.css, these use the fg-* prefix.

The fg-primary utility (lines 25–30) applies text-gray-900 in light mode and text-white in dark mode, ensuring icons remain visible regardless of theme:

@utility fg-primary {
  @apply text-gray-900;
  @variant theme-dark {
    @apply text-white;
  }
}

Text Utilities

Direct text color tokens reside in text-utils.css and use the text-* namespace. These differ from foreground utilities in that they target typography nodes (paragraphs, headings, links) rather than decorative elements.

The text-primary token (lines 1–6) mirrors the foreground pattern but applies specifically to typographic color inheritance, while specialized tokens like text-link (lines 33–38) provide semantic meaning for interactive elements:

@utility text-link {
  @apply text-blue-600;
  @variant theme-dark {
    @apply text-blue-500;
  }
}

Component Utilities

Higher-level UI abstractions live in component-utils.css, combining multiple lower-level tokens into composite utilities. These handle complex states like button backgrounds, tab indicators, and navigation highlights.

The button-bg-primary token (lines 2–9) demonstrates this layering:

@utility button-bg-primary {
  @apply bg-gray-900;
  @variant theme-dark {
    @apply bg-white;
  }
}

Additional variants like button-bg-primary-hover (lines 12–19) define interactive states, allowing developers to compose complete button styles through class combination rather than custom CSS.

Dark Mode Architecture

The design system centralizes dark mode handling through a custom theme variant triggered by the data-theme="dark" attribute on the root element (or any ancestor). Every functional token includes an @variant theme-dark block that overrides the base color.

This approach eliminates the need for manual dark mode logic in templates. Developers apply bg-surface once, and the system automatically renders the appropriate background color based on the current theme state. The variant definition relies on Tailwind's theme-dark custom variant, which queries the data-theme attribute.

Usage in Application Templates

Because tokens compile to standard Tailwind classes, they integrate seamlessly with Rails ViewComponents and ERB templates. Developers reference functional tokens directly in markup without importing additional stylesheets.

A typical container implementing the surface pattern uses:

<div class="bg-surface text-primary rounded-md p-4">
  <h2 class="text-primary">Welcome</h2>
  <button class="button-bg-primary hover:button-bg-primary-hover text-inverse rounded">
    Save
  </button>
</div>

In this example:

  • bg-surface provides the adaptive container background
  • text-primary ensures accessible contrast for headings
  • button-bg-primary and its hover variant handle interactive states
  • text-inverse (defined in foreground-utils.css lines 18–22) guarantees button text remains readable against both light and dark button backgrounds

Summary

  • Centralized imports: maybe-design-system.css serves as the single entry point for all token groups
  • CSS custom properties: Raw colors defined in the @theme block provide the foundation for all utilities
  • Functional tokens: The @utility directive creates semantic classes like bg-surface, fg-primary, and button-bg-primary
  • Theme-aware by default: Every token includes @variant theme-dark blocks that respond to data-theme="dark" attributes
  • Component-ready: Tokens compose in ERB templates and ViewComponents without additional configuration

Frequently Asked Questions

What is the difference between foreground and text utilities in Maybe's design system?

Foreground utilities (fg-*) target icons and decorative elements that sit inside containers, while text utilities (text-*) apply directly to typographic content. Both handle color adaptation, but they serve different semantic purposes—foreground utilities often pair with text-inverse to ensure contrast against colored backgrounds, whereas text utilities define the primary reading color for content blocks.

How does the design system detect and apply dark mode?

The system uses a custom Tailwind variant named theme-dark that activates when an ancestor element carries the data-theme="dark" attribute. Every functional token includes an @variant theme-dark block that overrides the base color, allowing the UI to switch themes without JavaScript manipulation of individual classes.

Where are the raw color values defined in the Maybe codebase?

Raw color values reside in the @theme block of app/assets/tailwind/maybe-design-system.css (lines 21–45). This declaration establishes CSS custom properties for the entire palette, which functional tokens in the utility files reference via var() notation or Tailwind's @apply directive.

Can I use these tokens outside of Rails ViewComponents?

Yes—the tokens compile to standard CSS classes during the Tailwind build process. While the Maybe app primarily consumes them in ERB templates and ViewComponents, any HTML markup can utilize classes like bg-surface or text-primary provided the compiled CSS is loaded. The tokens follow standard Tailwind conventions, making them portable to other contexts within the Rails asset pipeline.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →