Guide

Theming

Every component resolves through a theme — daisyUI by default, bare semantic HTML on demand.

The theme seam#

A theme is a map from component roles (:input, :select, :control, :label, :submit, :row, …) to component classes. The stable interface is the binding contract — the leaf initializer signatures (*modifiers, name:, id:, value:, error:, required:, …) — so the same form class renders under any theme.

With the daisyui gem loaded, the daisy theme is the default: every leaf delegates its markup and variant stacking to a real daisyUI component. Without it, the Plain theme takes over automatically. daisyui is a soft dependency — a non-daisyUI project never installs a UI kit it doesn't render.

The Plain theme#

Plain renders bare semantic HTML with zero styling classes and the same binding: names, ids, values, required, the hidden unchecked-value pair for checkboxes. Variants are accepted and ignored. Instead of styling, it emits stable hooks for your own CSS:

  • aria-invalid on invalid controls,
  • role="alert" + data-field-error on error messages,
  • data-field-hint on hints, data-form-row on rows,
  • a semantic fieldset/legend for groups, abbr for the required marker.

Documented degradations: searchable: selects fall back to the native select (no choices.js) and rich_textarea falls back to a plain textarea.

Selecting a theme#

render UserForm.new(model: @user, theme: :plain)   # per render

class AdminForm < Forms::Base
  form_options theme: :plain                       # per class
end

PhlexForms.configure { |c| c.theme = :plain }      # global default

theme: accepts :daisy, :plain, or a PhlexForms::Theme instance. Asking for :daisy without the daisyui gem raises a clear FeatureUnavailable.

Overriding single roles#

Theme#with swaps individual roles — your component just has to honor the role's initializer contract. Subclassing the shipped component is the easy way to stay contract-compatible:

config/initializers/phlex_forms.rb
class BrandInput < Forms::Plain::Input
  def view_template
    input(type: @type, value: @value.to_s, class: "brand-input", **unstyled_attributes.except(:class))
  end
end

PhlexForms.configure do |c|
  c.theme = PhlexForms::Theme.resolve(:plain).with(input: BrandInput)
end
The daisy leaves only reference DaisyUI::* inside view_template, so they load fine without the gem — they just must not be rendered. Theme resolution guarantees that.