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

```ruby
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:

```ruby
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
```

> **Note:** 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.