Hover Card

A popup that displays rich content when a trigger is hovered.

Loading...
<twig:HoverCard>
    <twig:HoverCard:Trigger>
        <twig:Button variant="link" {{ ...hover_card_trigger_attrs }}>Hover Here</twig:Button>
    </twig:HoverCard:Trigger>
    <twig:HoverCard:Content class="w-64 flex flex-col gap-0.5">
        <div class="font-semibold">@symfony</div>
        <div>The PHP framework for web applications — created by @fabpot.</div>
        <div class="mt-1 text-xs text-muted-foreground">Joined October 2010</div>
    </twig:HoverCard:Content>
</twig:HoverCard>

Installation

bin/console ux:install hover-card --kit shadcn

That's it!

Install the following Composer dependencies:

composer require tales-from-a-dev/twig-tailwind-extra:^1.0.0

Copy the following file(s) into your Symfony app:

assets/controllers/hover_card_controller.js
import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
    static values = {
        openDelay: { type: Number, default: 0 },
        closeDelay: { type: Number, default: 0 },
    };

    connect() {
        this.openTimeout = null;
        this.closeTimeout = null;
        this.element.dataset.state = 'closed';
    }

    disconnect() {
        this.#clearTimeouts();
    }

    show() {
        this.#clearTimeouts();
        this.openTimeout = setTimeout(() => {
            this.element.dataset.state = 'open';
            this.openTimeout = null;
        }, this.openDelayValue);
    }

    hide() {
        this.#clearTimeouts();
        this.closeTimeout = setTimeout(() => {
            this.element.dataset.state = 'closed';
            this.closeTimeout = null;
        }, this.closeDelayValue);
    }

    #clearTimeouts() {
        if (this.openTimeout) {
            clearTimeout(this.openTimeout);
            this.openTimeout = null;
        }
        if (this.closeTimeout) {
            clearTimeout(this.closeTimeout);
            this.closeTimeout = null;
        }
    }
}
templates/components/HoverCard.html.twig
{# @prop openDelay number Delay in milliseconds before showing the content. Defaults to `0` #}
{# @prop closeDelay number Delay in milliseconds before hiding the content. Defaults to `0` #}
{# @block content The hover card structure, typically a `HoverCard:Trigger` and `HoverCard:Content` #}
{%- props openDelay = 0, closeDelay = 0 -%}
<span
    class="{{ ('relative inline-block ' ~ attributes.render('class'))|tailwind_merge }}"
    {{ attributes.defaults({
        'data-slot': 'hover-card',
        'data-controller': 'hover-card',
        'data-hover-card-open-delay-value': openDelay,
        'data-hover-card-close-delay-value': closeDelay,
        'data-action': 'mouseenter->hover-card#show mouseleave->hover-card#hide',
    }) }}
>
    {%- block content %}{% endblock -%}
</span>
templates/components/HoverCard/Content.html.twig
{# @block content The content revealed on hover #}
<span
    role="tooltip"
    class="{{ ('invisible opacity-0 in-data-[state=open]:visible in-data-[state=open]:opacity-100 transition-opacity absolute left-1/2 top-full z-50 mt-2 w-64 -translate-x-1/2 rounded-md border bg-popover p-4 text-sm text-popover-foreground shadow-md outline-none ' ~ attributes.render('class'))|tailwind_merge }}"
    {{ attributes.defaults({
        'data-slot': 'hover-card-content',
    }) }}
>
    {%- block content %}{% endblock -%}
</span>
templates/components/HoverCard/Trigger.html.twig
{# @block content The element that reveals the hover card on hover or focus #}
{%- set hover_card_trigger_attrs = {
    'data-slot': 'hover-card-trigger',
    tabindex: 0,
    'data-action': 'focus->hover-card#show blur->hover-card#hide'|html_attr_type('sst'),
} -%}
{%- block content %}{% endblock -%}

Happy coding!

Usage

<twig:HoverCard>
    <twig:HoverCard:Trigger>
        <a href="#" class="underline" {{ ...hover_card_trigger_attrs }}>@symfony</a>
    </twig:HoverCard:Trigger>
    <twig:HoverCard:Content>
        The Symfony PHP framework — official organization on GitHub.
    </twig:HoverCard:Content>
</twig:HoverCard>

Examples

Sides

Loading...
<div class="flex flex-wrap justify-center gap-12 py-12">
    {# Left #}
    <twig:HoverCard>
        <twig:HoverCard:Trigger>
            <twig:Button variant="outline" {{ ...hover_card_trigger_attrs }}>Left</twig:Button>
        </twig:HoverCard:Trigger>
        <twig:HoverCard:Content class="top-1/2 left-auto right-full translate-x-0 -translate-y-1/2 mt-0 mr-2">
            <div class="flex flex-col gap-1">
                <h4 class="font-medium">Hover Card</h4>
                <p>This hover card appears on the left side of the trigger.</p>
            </div>
        </twig:HoverCard:Content>
    </twig:HoverCard>

    {# Top #}
    <twig:HoverCard>
        <twig:HoverCard:Trigger>
            <twig:Button variant="outline" {{ ...hover_card_trigger_attrs }}>Top</twig:Button>
        </twig:HoverCard:Trigger>
        <twig:HoverCard:Content class="top-auto bottom-full mt-0 mb-2">
            <div class="flex flex-col gap-1">
                <h4 class="font-medium">Hover Card</h4>
                <p>This hover card appears on the top side of the trigger.</p>
            </div>
        </twig:HoverCard:Content>
    </twig:HoverCard>

    {# Bottom (default) #}
    <twig:HoverCard>
        <twig:HoverCard:Trigger>
            <twig:Button variant="outline" {{ ...hover_card_trigger_attrs }}>Bottom</twig:Button>
        </twig:HoverCard:Trigger>
        <twig:HoverCard:Content>
            <div class="flex flex-col gap-1">
                <h4 class="font-medium">Hover Card</h4>
                <p>This hover card appears on the bottom side of the trigger.</p>
            </div>
        </twig:HoverCard:Content>
    </twig:HoverCard>

    {# Right #}
    <twig:HoverCard>
        <twig:HoverCard:Trigger>
            <twig:Button variant="outline" {{ ...hover_card_trigger_attrs }}>Right</twig:Button>
        </twig:HoverCard:Trigger>
        <twig:HoverCard:Content class="top-1/2 left-full translate-x-0 -translate-y-1/2 mt-0 ml-2">
            <div class="flex flex-col gap-1">
                <h4 class="font-medium">Hover Card</h4>
                <p>This hover card appears on the right side of the trigger.</p>
            </div>
        </twig:HoverCard:Content>
    </twig:HoverCard>
</div>

RTL

Loading...
<div class="flex flex-col items-center gap-24 py-12">
    {# Arabic #}
    <div class="flex flex-wrap justify-center gap-2" dir="rtl">
        <twig:HoverCard>
            <twig:HoverCard:Trigger>
                <twig:Button variant="outline" {{ ...hover_card_trigger_attrs }}>يسار</twig:Button>
            </twig:HoverCard:Trigger>
            <twig:HoverCard:Content class="top-1/2 left-auto right-full translate-x-0 -translate-y-1/2 mt-0 mr-2 w-64 flex flex-col gap-1">
                <div class="font-semibold">سماعات لاسلكية</div>
                <div class="text-sm text-muted-foreground">٩٩.٩٩ $</div>
            </twig:HoverCard:Content>
        </twig:HoverCard>

        <twig:HoverCard>
            <twig:HoverCard:Trigger>
                <twig:Button variant="outline" {{ ...hover_card_trigger_attrs }}>أعلى</twig:Button>
            </twig:HoverCard:Trigger>
            <twig:HoverCard:Content class="top-auto bottom-full mt-0 mb-2 w-64 flex flex-col gap-1">
                <div class="font-semibold">سماعات لاسلكية</div>
                <div class="text-sm text-muted-foreground">٩٩.٩٩ $</div>
            </twig:HoverCard:Content>
        </twig:HoverCard>

        <twig:HoverCard>
            <twig:HoverCard:Trigger>
                <twig:Button variant="outline" {{ ...hover_card_trigger_attrs }}>أسفل</twig:Button>
            </twig:HoverCard:Trigger>
            <twig:HoverCard:Content class="w-64 flex flex-col gap-1">
                <div class="font-semibold">سماعات لاسلكية</div>
                <div class="text-sm text-muted-foreground">٩٩.٩٩ $</div>
            </twig:HoverCard:Content>
        </twig:HoverCard>

        <twig:HoverCard>
            <twig:HoverCard:Trigger>
                <twig:Button variant="outline" {{ ...hover_card_trigger_attrs }}>يمين</twig:Button>
            </twig:HoverCard:Trigger>
            <twig:HoverCard:Content class="top-1/2 left-full translate-x-0 -translate-y-1/2 mt-0 ml-2 w-64 flex flex-col gap-1">
                <div class="font-semibold">سماعات لاسلكية</div>
                <div class="text-sm text-muted-foreground">٩٩.٩٩ $</div>
            </twig:HoverCard:Content>
        </twig:HoverCard>
    </div>

    {# Hebrew #}
    <div class="flex flex-wrap justify-center gap-2" dir="rtl">
        <twig:HoverCard>
            <twig:HoverCard:Trigger>
                <twig:Button variant="outline" {{ ...hover_card_trigger_attrs }}>שמאל</twig:Button>
            </twig:HoverCard:Trigger>
            <twig:HoverCard:Content class="top-1/2 left-auto right-full translate-x-0 -translate-y-1/2 mt-0 mr-2 w-64 flex flex-col gap-1">
                <div class="font-semibold">אוזניות אלחוטיות</div>
                <div class="text-sm text-muted-foreground">99.99 $</div>
            </twig:HoverCard:Content>
        </twig:HoverCard>

        <twig:HoverCard>
            <twig:HoverCard:Trigger>
                <twig:Button variant="outline" {{ ...hover_card_trigger_attrs }}>למעלה</twig:Button>
            </twig:HoverCard:Trigger>
            <twig:HoverCard:Content class="top-auto bottom-full mt-0 mb-2 w-64 flex flex-col gap-1">
                <div class="font-semibold">אוזניות אלחוטיות</div>
                <div class="text-sm text-muted-foreground">99.99 $</div>
            </twig:HoverCard:Content>
        </twig:HoverCard>

        <twig:HoverCard>
            <twig:HoverCard:Trigger>
                <twig:Button variant="outline" {{ ...hover_card_trigger_attrs }}>למטה</twig:Button>
            </twig:HoverCard:Trigger>
            <twig:HoverCard:Content class="w-64 flex flex-col gap-1">
                <div class="font-semibold">אוזניות אלחוטיות</div>
                <div class="text-sm text-muted-foreground">99.99 $</div>
            </twig:HoverCard:Content>
        </twig:HoverCard>

        <twig:HoverCard>
            <twig:HoverCard:Trigger>
                <twig:Button variant="outline" {{ ...hover_card_trigger_attrs }}>ימין</twig:Button>
            </twig:HoverCard:Trigger>
            <twig:HoverCard:Content class="top-1/2 left-full translate-x-0 -translate-y-1/2 mt-0 ml-2 w-64 flex flex-col gap-1">
                <div class="font-semibold">אוזניות אלחוטיות</div>
                <div class="text-sm text-muted-foreground">99.99 $</div>
            </twig:HoverCard:Content>
        </twig:HoverCard>
    </div>
</div>

API Reference

HoverCard

Prop Type Description
openDelay number Delay in milliseconds before showing the content. Defaults to 0
closeDelay number Delay in milliseconds before hiding the content. Defaults to 0
Block Description
content The hover card structure, typically a HoverCard:Trigger and HoverCard:Content

HoverCard:Content

Block Description
content The content revealed on hover

HoverCard:Trigger

Block Description
content The element that reveals the hover card on hover or focus