View as Markdown

Hover Card

For sighted users to preview content available behind a link.

Loading...
<twig:HoverCard openDelay="10" closeDelay="100">
    <twig:HoverCard:Trigger>
        <twig:Button variant="link" {{ ...hover_card_trigger_attrs }}>Hover Here</twig:Button>
    </twig:HoverCard:Trigger>
    <twig:HoverCard:Content class="flex w-64 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

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

Install the following Composer dependencies:

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

Copy the following file(s) into your app:

import { Controller } from &#039;@hotwired/stimulus&#039;;

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 = &#039;closed&#039;;
    }

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

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

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

    #clearTimeouts() {
        if (this.openTimeout) {
            clearTimeout(this.openTimeout);
            this.openTimeout = null;
        }
        if (this.closeTimeout) {
            clearTimeout(this.closeTimeout);
            this.closeTimeout = null;
        }
    }
}
{# @prop openDelay number Delay in milliseconds before showing the content. #}
{# @prop closeDelay number Delay in milliseconds before hiding the content. #}
{# @block content The hover card structure, typically a `HoverCard:Trigger` and `HoverCard:Content`. #}
{%- props openDelay = 0, closeDelay = 0 -%}
&lt;span
    class=&quot;{{ (&#039;relative inline-block &#039; ~ attributes.render(&#039;class&#039;))|tailwind_merge }}&quot;
    {{ attributes.defaults({
        &#039;data-slot&#039;: &#039;hover-card&#039;,
        &#039;data-controller&#039;: &#039;hover-card&#039;,
        &#039;data-hover-card-open-delay-value&#039;: openDelay,
        &#039;data-hover-card-close-delay-value&#039;: closeDelay,
        &#039;data-action&#039;: &#039;mouseenter-&gt;hover-card#show mouseleave-&gt;hover-card#hide&#039;,
    }) }}
&gt;
    {%- block content %}{% endblock -%}
&lt;/span&gt;
{# @prop side &#039;bottom&#039;|&#039;top&#039;|&#039;left&#039;|&#039;right&#039; The side where the content appears. #}
{# @block content The content revealed on hover. #}
{%- props side = &#039;bottom&#039; -%}
{%- set style = html_cva(
    base: &#039;invisible opacity-0 in-data-[state=open]:visible in-data-[state=open]:opacity-100 transition-opacity duration-100 absolute z-50 w-64 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden &#039;,
    variants: {
        side: {
            bottom: &#039;left-1/2 top-full mt-2 -translate-x-1/2&#039;,
            top: &#039;left-1/2 top-auto bottom-full mb-2 mt-0 -translate-x-1/2&#039;,
            left: &#039;top-1/2 left-auto right-full -translate-y-1/2 translate-x-0 mt-0 mr-2&#039;,
            right: &#039;top-1/2 left-full -translate-y-1/2 translate-x-0 mt-0 ml-2&#039;,
        },
    },
    default_variant: {
        side: &#039;bottom&#039;,
    },
) -%}
&lt;span
    role=&quot;tooltip&quot;
    data-side=&quot;{{ side }}&quot;
    class=&quot;{{ style.apply({side: side}, attributes.render(&#039;class&#039;))|tailwind_merge }}&quot;
    {{ attributes.defaults({
        &#039;data-slot&#039;: &#039;hover-card-content&#039;,
    }) }}
&gt;
    {%- block content %}{% endblock -%}
&lt;/span&gt;
{# @block content The element that reveals the hover card on hover or focus. #}
{%- set hover_card_trigger_attrs = {
    &#039;data-slot&#039;: &#039;hover-card-trigger&#039;,
    tabindex: 0,
    &#039;data-action&#039;: &#039;focus-&gt;hover-card#show blur-&gt;hover-card#hide&#039;|html_attr_type(&#039;sst&#039;),
} -%}
{%- block content %}{% endblock -%}

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

Basic

Loading...
<twig:HoverCard openDelay="10" closeDelay="100">
    <twig:HoverCard:Trigger>
        <twig:Button variant="link" {{ ...hover_card_trigger_attrs }}>Hover Here</twig:Button>
    </twig:HoverCard:Trigger>
    <twig:HoverCard:Content class="flex w-64 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>

Sides

Loading...
<div class="flex flex-wrap justify-center gap-2">
    {% for side in ['left', 'top', 'bottom', 'right'] %}
        <twig:HoverCard openDelay="100" closeDelay="100">
            <twig:HoverCard:Trigger>
                <twig:Button variant="outline" class="capitalize" {{ ...hover_card_trigger_attrs }}>{{ side }}</twig:Button>
            </twig:HoverCard:Trigger>
            <twig:HoverCard:Content side="{{ side }}">
                <div class="flex flex-col gap-1">
                    <h4 class="font-medium">Hover Card</h4>
                    <p>This hover card appears on the {{ side }} side of the trigger.</p>
                </div>
            </twig:HoverCard:Content>
        </twig:HoverCard>
    {% endfor %}
</div>

RTL

To enable RTL support, set the dir="rtl" attribute on the root element.

Loading...
<div class="flex flex-col items-center gap-24 py-12">
    {# Arabic #}
    <div class="flex flex-wrap justify-center gap-2" dir="rtl">
        {% for side, label in {left: 'يسار', top: 'أعلى', bottom: 'أسفل', right: 'يمين'} %}
            <twig:HoverCard openDelay="10" closeDelay="100">
                <twig:HoverCard:Trigger>
                    <twig:Button variant="outline" {{ ...hover_card_trigger_attrs }}>{{ label }}</twig:Button>
                </twig:HoverCard:Trigger>
                <twig:HoverCard:Content side="{{ side }}" class="flex w-64 flex-col gap-1">
                    <div class="font-semibold">سماعات لاسلكية</div>
                    <div class="text-sm text-muted-foreground">٩٩.٩٩ $</div>
                </twig:HoverCard:Content>
            </twig:HoverCard>
        {% endfor %}
    </div>

    {# Hebrew #}
    <div class="flex flex-wrap justify-center gap-2" dir="rtl">
        {% for side, label in {left: 'שמאל', top: 'למעלה', bottom: 'למטה', right: 'ימין'} %}
            <twig:HoverCard openDelay="10" closeDelay="100">
                <twig:HoverCard:Trigger>
                    <twig:Button variant="outline" {{ ...hover_card_trigger_attrs }}>{{ label }}</twig:Button>
                </twig:HoverCard:Trigger>
                <twig:HoverCard:Content side="{{ side }}" class="flex w-64 flex-col gap-1">
                    <div class="font-semibold">אוזניות אלחוטיות</div>
                    <div class="text-sm text-muted-foreground">99.99 $</div>
                </twig:HoverCard:Content>
            </twig:HoverCard>
        {% endfor %}
    </div>
</div>

API Reference

<twig:HoverCard>

Prop Type Default
openDelay 
number 0
closeDelay 
number 0
Block Description
content The hover card structure, typically a HoverCard:Trigger and HoverCard:Content.

<twig:HoverCard:Content>

Prop Type Default
side 
'bottom'|'top'|'left'|'right' 'bottom'
Block Description
content The content revealed on hover.

<twig:HoverCard:Trigger>

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