Toolbar
Beta

Popover

Low-level primitive for custom contextual surfaces that do not fit a specialized Frakton NG component.

Import

import {FktPopoverComponent} from "frakton-ng/popover";

Usage

Popover is a low-level primitive for custom contextual surfaces that do not fit a specialized Frakton NG component. Prefer Tooltip, Dialog, Select, Autocomplete, and other dedicated components when their interaction pattern matches the use case.

It renders content in the browser top layer without moving it to a global overlay container, so tokens, inherited classes, scoped styles, and animation classes keep flowing through the normal cascade. Popover owns disclosure and placement mechanics, but it does not assign menu, tooltip, dialog, or listbox semantics to arbitrary content.

Apply fktPopoverTrigger to the interactive control that owns the surface. Frakton synchronizes aria-expanded and aria-controls; custom patterns remain responsible for the panel role and name, aria-haspopup when applicable, internal keyboard behavior, and additional focus management.

Basic

The default trigger interaction opens on click, closes on outside click or Escape, and uses bottom-center placement. The trigger remains the real interactive element and the content template becomes the native popover panel. Apply fktPopoverTrigger to a keyboard-accessible interactive element; the directive does not turn passive elements into controls.


  

  
    
Basic popover

Popover content rendered from a trigger-based template.

Hover and focus

Use triggerOn="hover" on the trigger for lightweight pointer and keyboard preview interactions. Focus can move into the panel, and pointer movement between trigger and panel is protected by a safe area. Prefer click for persistent, touch-first, or complex interactive flows.


  

  
    
Accessible hover

The same trigger opens on mouse hover and keyboard focus, then closes when pointer and focus leave both trigger and panel.

Forms

Popover content is just Angular template content, so compact forms can live inside the panel without a special API.


  

  
    
@if (submitted(); as value) { }

Positioning

Configure where the panel appears relative to the trigger and how it reacts when the requested placement does not fit.

Positions

Positions describe where the panel sits around the trigger. top and bottom are block sides; start and end are inline sides. Corner positions use the *-corner suffix.

@for (group of groups; track group.label) {
  
{{ group.label }}
@for (position of group.positions; track position) {
{{ position.label }} {{ position.description }}
}
}

Rtl positioning

Logical start and end positions follow the trigger direction by default. Use positionDirection="ltr" or positionDirection="rtl" when geometry should be fixed independently from the content direction.



Automatic content direction ({{ direction() === 'rtl' ? 'RTL' : 'LTR' }}) The positioning follows the selected direction. With positionDirection="auto", end-center resolves from the trigger direction.
موضع تلقائي

The content and trigger share the selected direction, so logical end follows the same flow.

Fixed LTR positioning with {{ direction() === 'rtl' ? 'RTL' : 'LTR' }} content The content follows the selected direction, but positionDirection="ltr" keeps end-center resolved as LTR geometry.
محتوى RTL

Direction can be fixed for geometry without changing the content direction inside the panel.

Reposition

Use overflowStrategy="keep-position" when the preferred placement should be preserved, and call repositionTo('fit') when content or layout changes after the popover is open. Programmatic repositioning persists the resolved placement as the active preference.

Manual reposition

Overflow strategy is keeping the requested placement, so the preferred position is preserved even if it overflows.

Trigger relative sizing

Use --fkt-popover-trigger-width with panel sizing tokens when the panel should derive its width from the trigger. This keeps sizing in CSS instead of adding one-off width inputs.

@for (example of examples; track example.label) {
  
    

    
      
{{ example.label }} {{ example.description }} {{ example.token }}
}

Dismiss behavior

Automatic dismiss can be configured independently from state. dismiss reports the automatic reason, while content actions close by updating the open state. Partial dismissOn objects merge with the defaults, so omitted options keep their default behavior. Focus restoration is explicit: Escape returns focus to the trigger, and content actions can call restoreTriggerFocus() when returning keyboard users to the trigger is the desired flow.


  

  
    
Dismiss behavior
        Outside click, Escape and scroll dismiss this popover.

        Partial dismissOn values merge with the defaults.

        Try to scroll, type ESC or click outside
      

State

Bind [(open)] when a parent component should observe or change the popover state directly.

Programmatic state

Bind [(open)] when the parent should observe or update the state. Use triggerOn="manual" when the trigger's own handler or parent state should control activation. The directive still belongs on the real interactive control and continues to provide aria-expanded, aria-controls, and the positioning reference. This is not a separate controlled mode; trigger and dismiss behavior remain configured independently.

Programmatic state The parent owns the open signal. Manual mode leaves activation to the trigger handler while Frakton keeps ARIA state and positioning connected to that same control.

Customization

Customize the panel surface and motion with CSS tokens and native popover animation selectors.

Design tokens

The panel has a light default appearance and exposes styling hooks through CSS tokens instead of TypeScript style props.


  

  
    
Scoped tokens The panel appearance comes from CSS variables.

Animations

Motion is class-based because the panel is a native popover. Use the built-in fade-slide, disable it with none, or pass custom classes and animate with browser-native tools such as :popover-open, @starting-style, allow-discrete, and transitions for display or overlay. Keep animation classes local with fkt-popover ::ng-deep .my-animation-class, or put shared animation classes in styles.css when they are reused across the app.

Uses the built-in fade-slide animation.

Uses a consumer-owned class for transition and starting style.

Uses a consumer-owned class for transition and starting style.

Disables the built-in animation class.

Popover

API Reference

fkt-popover is a low-level primitive for custom contextual surfaces that do not fit a specialized Frakton NG component. It manages disclosure, placement, automatic dismiss behavior, and optional focus restoration.

Prefer dedicated components such as Tooltip, Dialog, Select, and Autocomplete when their interaction pattern matches the use case. Those components can provide semantics and keyboard behavior that an arbitrary popover cannot infer.

Use fktPopoverTrigger on the trigger element and fktPopoverContent on an ng-template that contains the panel content.


<fkt-popover>
	<button fktButton fktPopoverTrigger label="Open popover"></button>

	<ng-template fktPopoverContent>
		<p>Popover content</p>
	</ng-template>
</fkt-popover>

Native surface

The panel uses the browser Popover API. It participates in the top layer, is not portaled to a global overlay container, and supports native CSS states such as :popover-open, @starting-style, and discrete transitions for display and overlay.

Frakton NG owns trigger-based positioning because native popover does not solve trigger placement by itself.

Structure constraints

fkt-popover expects one fktPopoverTrigger and one fktPopoverContent. Missing trigger or content is a configuration error. If either is conditionally rendered, ensure it exists before opening the popover.

Semantics

Popover guarantees the mechanics it owns: it keeps aria-expanded and aria-controls synchronized on the trigger, supports focus as the keyboard equivalent of hover, dismisses with Escape by default, and provides explicit trigger focus restoration.

The panel does not assign a semantic role or accessible name. Popover also does not infer aria-haspopup, implement keyboard navigation inside projected content, trap focus, or turn arbitrary markup into a menu, listbox, dialog, tooltip, or other composite widget. When those semantics are required, prefer the dedicated Frakton NG component. For a custom pattern, provide the panel role, accessible name, aria-haspopup value, internal keyboard behavior, and focus management required by the chosen pattern.

Trigger behavior

Apply fktPopoverTrigger to the interactive control that owns the popover. The trigger receives aria-expanded and aria-controls and also acts as the positioning reference.

The directive does not infer whether an element is interactive and does not add a role, tabindex, or keyboard activation to passive elements. Use a native interactive element when possible. Custom controls must provide their own correct semantics and keyboard behavior.

triggerOn="hover" also opens on focus. The popover remains open while pointer or focus stays inside the trigger or panel, and pointer movement across the offset gap is protected by a safe area. Prefer click for persistent, touch-first, or complex interactive flows.

triggerOn="manual" disables only the activation performed by the directive. Use the trigger's own event handler or [(open)] for state changes. The directive must remain on the real interactive control: it still owns aria-expanded, aria-controls, and the positioning reference in manual mode.

triggerDisabled disables trigger interaction only. It does not close an open panel and does not prevent external open updates.

There is no global disabled state. Use triggerDisabled for trigger-driven interaction and control open externally when the whole flow should be unavailable.

Dismiss behavior

dismissOn configures automatic dismiss triggers. Partial values are merged with the default dismiss configuration, so omitted options keep their default behavior.


<fkt-popover
	[dismissOn]="{
    outsideClick: false
  }"
>
	...
</fkt-popover>

dismiss emits only for automatic dismiss events. Programmatic state changes through [(open)] are not emitted as dismiss events.

By default, scrolling repositions the panel instead of dismissing it. When dismissOn.scroll is enabled, external scroll dismisses the popover. Scroll events from inside the trigger or panel are ignored, so scrollable content inside the popover remains usable.

Restoring trigger focus

Close the popover by updating open. When the same action should return keyboard focus to the trigger, call restoreTriggerFocus() after changing state.


<fkt-popover #popover="fktPopover" [(open)]="open">
	...

	<button
		fktButton
		label="Apply"
		(click)="open.set(false); popover.restoreTriggerFocus()"
	></button>
</fkt-popover>

Escape restores focus to the trigger by default. Outside click, scroll, mouse leave, focus out, and programmatic state changes do not restore focus automatically.

Positioning

preferredPosition is the declarative preferred placement. preferredFallbackPositions adds preferred alternatives before the automatic fit search tries other placements.

With overflowStrategy="fit", the popover may resolve to a different placement when the preferred placement does not fit. It tries the declared position, then preferred fallbacks, then an internal fit strategy. preferredFallbackPositions only affects this automatic strategy. With overflowStrategy="keep-position", the requested placement is preserved even when it overflows.


<fkt-popover
	preferredPosition="bottom-start"
	[preferredFallbackPositions]="['top-start', 'bottom-end']"
	overflowStrategy="fit"
>
	...
</fkt-popover>

Positions use logical sides. top and bottom are block sides. start and end are inline sides. center, top, and bottom align the panel along the selected side. Corner placements such as top-start-corner, top-end-corner, bottom-start-corner, and bottom-end-corner are separate positions around trigger corners.

The current placement is exposed as data-fkt-position and data-fkt-position-direction on the host and panel for placement-specific styling, and through resolvedPosition for Angular state.

positionDirection controls how logical start and end positions are resolved. The default auto value reads the trigger computed direction. Use ltr or rtl to force the positioning direction for a specific popover.

Position update lifecycle

The popover repositions while open on document scroll, window resize, and trigger resize.

Panel content size is consumer-owned. Prefer fixed or constrained panel dimensions for dynamic content, and let the content scroll inside the panel when it can grow. If an intentional content layout change should re-evaluate placement, call repositionTo('fit') to run the automatic fit search and persist the resolved placement as the active preference.

Automatic collision handling updates the active placement while the panel is open without changing the active preference. Programmatic repositioning updates the active preference to the resolved placement. Neither writes back to the preferredPosition input; when the preferredPosition input changes, the active placement is recalculated from the new input value.

Content lifecycle

The content template is instantiated with the popover component and remains mounted while the popover exists. Opening and closing use the native Popover API and do not destroy Angular content. Internal component state, form state, subscriptions, and DOM state inside the content are preserved between openings.

Because content remains mounted, expensive content should be conditionally instantiated by the consumer when needed.

Sizing and styling

The host exposes --fkt-popover-trigger-width and --fkt-popover-trigger-height. Combine those variables with sizing tokens for proportional panel sizes.

.popover-demo {
	--fkt-popover-width: var(--fkt-popover-trigger-width);
	--fkt-popover-max-height: 24rem;
}

Use the content template for internal layout. The panel itself is customized through tokens for structural surface decisions such as size, background, border, radius, shadow, and padding.

Animations

Motion is class-based. Use the built-in fade-slide, disable animation with none, or pass custom classes. Custom values are appended to the panel class list, so a value such as "app-popover-motion app-popover-danger" applies both classes.


<fkt-popover animation="app-menu-motion">
	...
</fkt-popover>

Custom animation classes can use native popover selectors and transitions. Use ::ng-deep for local animation classes. Use global styles when the animation is shared across the app.

The built-in animation respects prefers-reduced-motion; custom animations should do the same when they include meaningful movement.

:host ::ng-deep .app-menu-motion {
	opacity: 0;
	transform: scale(0.96);
	transition: opacity 160ms ease-out,
	transform 160ms ease-out,
	display 160ms allow-discrete,
	overlay 160ms allow-discrete;
}

:host ::ng-deep .app-menu-motion:popover-open {
	opacity: 1;
	transform: scale(1);
}
/* styles.css */
.app-menu-motion {
	opacity: 0;
	transform: scale(0.96);
}

Methods

class FktPopoverComponent {
  repositionTo(target: FktPopoverRepositionTarget): void;

  restoreTriggerFocus(): void;
}

repositionTo recalculates placement while the panel is open and persists the resolved placement as the active preference. Pass 'fit' to resolve through the fit search. Pass a concrete position to use that placement directly. The method does not write back to the preferredPosition input.

restoreTriggerFocus moves focus back to the trigger. Use it after state-driven closes when returning focus to the trigger is the desired flow.

Public types

type FktPopoverPosition =
	| 'top-start'
	| 'top-center'
	| 'top-end'
	| 'top-start-corner'
	| 'top-end-corner'
	| 'bottom-start'
	| 'bottom-center'
	| 'bottom-end'
	| 'bottom-start-corner'
	| 'bottom-end-corner'
	| 'start-top'
	| 'start-center'
	| 'start-bottom'
	| 'end-top'
	| 'end-center'
	| 'end-bottom';

type FktPopoverTrigger = 'click' | 'hover' | 'manual';

type FktPopoverAnimation =
	| 'fade-slide'
	| 'none'
	| (string & {});

type FktPopoverOverflowStrategy = 'fit' | 'keep-position';

type FktPopoverRepositionTarget =
	| FktPopoverPosition
	| 'fit';

type FktPopoverPositionDirection = 'auto' | 'ltr' | 'rtl';

interface FktPopoverDismissOn {
	outsideClick?: boolean;
	escape?: boolean;
	scroll?: boolean;
}

type FktPopoverDismissReason =
	| 'outside-click'
	| 'escape'
	| 'scroll'
	| 'mouse-leave'
	| 'focus-out';

interface FktPopoverDismissEvent {
	reason: FktPopoverDismissReason;
	sourceEvent?: Event;
}