ComponentsThemes
Palette
Default
Stone
Rose
Blue
Green
Violet
Yellow
Orange
6

← Components

Tags Input

Multi-value text entry that collects free-form tags, with edit-in-place and array form submission.

Installation

bin/rails g wabi:add tags_input

Example

Limiting tags

Cap the number of tags with max:. Pass editable: false to prevent editing existing tags in place.

Disabled

Source

app/components/ui/tags_input.rb

# frozen_string_literal: true

require "json"
require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class TagsInput < Wabi::Base
      def initialize(name: nil, value: [], max: nil, editable: true, disabled: false, placeholder: nil, **attrs)
        @name        = name
        @value       = Array(value)
        @max         = max
        @editable    = editable
        @disabled    = disabled
        @placeholder = placeholder
        @attrs       = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        div(
          **@attrs,
          data: {
            controller: "wabi--tags-input",
            "wabi--tags-input-name-value":        @name,
            "wabi--tags-input-value-value":       @value.to_json,
            "wabi--tags-input-max-value":         @max.to_s,
            "wabi--tags-input-editable-value":    @editable.to_s,
            "wabi--tags-input-disabled-value":    @disabled.to_s,
            "wabi--tags-input-placeholder-value": @placeholder.to_s,
          },
          class: merge_class("flex flex-col gap-2", user_class)
        ) do
          if block
            yield
          else
            render TagsInputControl.new do
              render TagsInputInput.new(placeholder: @placeholder)
            end
          end
        end
      end
    end
  end
end

app/components/ui/tags_input_control.rb

# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class TagsInputControl < Wabi::Base
      def initialize(**attrs)
        @attrs = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        div(
          data: { "wabi--tags-input-target": "control" },
          class: merge_class(
            "flex min-h-10 w-full flex-wrap items-center gap-1.5 rounded-md border border-input " \
            "bg-background px-3 py-2 text-sm ring-offset-background " \
            "focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 " \
            "data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50",
            user_class
          )
        ) do
          yield if block_given?
        end
      end
    end
  end
end

app/components/ui/tags_input_input.rb

# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class TagsInputInput < Wabi::Base
      def initialize(placeholder: nil, **attrs)
        @placeholder = placeholder
        @attrs       = attrs
      end

      def view_template
        user_class = @attrs.delete(:class)
        input(
          type: "text",
          placeholder: @placeholder,
          data: { "wabi--tags-input-target": "input" },
          class: merge_class(
            "flex-1 min-w-[6rem] bg-transparent outline-none placeholder:text-muted-foreground " \
            "disabled:cursor-not-allowed",
            user_class
          )
        )
      end
    end
  end
end

app/components/ui/tags_input_error.rb

# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class TagsInputError < Wabi::Base
      def initialize(**attrs)
        @attrs = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        p(
          role: "alert",
          data: { "wabi--tags-input-target": "error" },
          class: merge_class("text-sm font-medium text-destructive", user_class)
        ) do
          yield if block_given?
        end
      end
    end
  end
end

app/components/ui/tags_input_label.rb

# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class TagsInputLabel < Wabi::Base
      def initialize(**attrs)
        @attrs = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        label(
          data: { "wabi--tags-input-target": "label" },
          class: merge_class("text-sm font-medium leading-none", user_class)
        ) do
          yield if block_given?
        end
      end
    end
  end
end

Accessibility

  • Backspace on an empty input removes the last tag; arrow keys move between tags.
  • Each tag exposes a labelled delete button.
  • Submits as an array: params[:tags] # => ["ruby", "rails"].
  • Disabled tags render with muted text; per WCAG 1.4.3 inactive/disabled UI components are exempt from the contrast minimum.