ComponentsThemes
Palette
Default
Stone
Rose
Blue
Green
Violet
Yellow
Orange
6

← Components

Hover Card

Rich preview card shown on hover or focus of a trigger, rendered through a portal.

Installation

bin/rails g wabi:add hover_card

Example

Custom delays

Tune open_delay: and close_delay: (milliseconds) to make the card appear or dismiss faster. Defaults are 700 / 300.

Source

app/components/ui/hover_card.rb

# frozen_string_literal: true

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

module Components
  module UI
    class HoverCard < Wabi::Base
      def initialize(id: nil, open_delay: 700, close_delay: 300, portal: true, **attrs)
        @id          = id
        @open_delay  = open_delay
        @close_delay = close_delay
        @portal      = portal
        @attrs       = attrs
      end

      def view_template(&block)
        div(
          id: @id,
          class: "inline-block",
          data: {
            controller: "wabi--hover-card",
            "wabi--hover-card-open-delay-value":  @open_delay.to_s,
            "wabi--hover-card-close-delay-value": @close_delay.to_s,
            "wabi--hover-card-portal-value":      @portal.to_s,
          }
        ) do
          yield if block_given?
        end
      end
    end
  end
end

app/components/ui/hover_card_trigger.rb

# frozen_string_literal: true

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

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

      def view_template(&block)
        user_class = @attrs.delete(:class)
        button(
          type: "button",
          # aria-haspopup signals to AT that this trigger opens a popup panel.
          # Zag never supplies this attribute via getTriggerProps(), so we set
          # it statically here. Callers may override via attrs if needed.
          "aria-haspopup": "true",
          **@attrs,
          data: { "wabi--hover-card-target": "trigger" },
          class: merge_class("inline-flex items-center underline-offset-4 hover:underline cursor-pointer", user_class)
        ) do
          yield if block_given?
        end
      end
    end
  end
end

app/components/ui/hover_card_content.rb

# frozen_string_literal: true

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

module Components
  module UI
    class HoverCardContent < Wabi::Base
      variants do
        base "z-50 w-64 rounded-md border border-input bg-popover p-4 text-popover-foreground shadow-md outline-none " \
             "transition-opacity duration-200 ease-out motion-reduce:transition-none " \
             "data-[state=open]:opacity-100 data-[state=closed]:opacity-0 " \
             "data-[state=closed]:pointer-events-none"
      end

      def initialize(**attrs)
        @attrs = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        div(
          data: { "wabi--hover-card-target": "positioner" },
          class: "z-50 pointer-events-none"
        ) do
          div(
            data: { "wabi--hover-card-target": "content" },
            "data-state": "closed",
            inert: true,
            class: merge_class(tokens, user_class)
          ) do
            yield if block_given?
          end
        end
      end
    end
  end
end

Accessibility

  • Opens on pointer hover and on keyboard focus of the trigger.
  • Content is inert and aria-hidden while closed; not a focus trap (non-modal).
  • Respects prefers-reduced-motion (fade transition is disabled).