npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

layercraft

v1.0.0

Published

A lightweight positioning library for floating elements like tooltips, popovers, and dropdowns

Readme

Layercraft

A positioning engine for floating UI elements. Built to solve real problems with tooltips, dropdowns, and popovers — without the magic.

npm version npm bundle size CI TypeScript license

Why this exists

Positioning floating elements sounds simple until you hit:

  • overflow: hidden clipping your dropdown
  • z-index wars across stacking contexts
  • Tooltips getting cut off at viewport edges
  • Initial flicker before position is calculated
  • Mysterious jumps when content resizes

Existing solutions (Popper.js, Floating UI) work, but they're often opaque. When something breaks, good luck debugging.

Layercraft takes a different approach: explicit math, predictable behavior, no magic.

Features

  • 🎯 12 placement options — top, bottom, left, right with start/center/end alignment
  • 🔄 Auto-flip — automatically flips when there's not enough space
  • 🔀 Shift — keeps element within viewport without changing placement
  • 📍 Fallback placements — custom flip order for complex layouts
  • 🚀 Portal support — escape overflow: hidden containers
  • ➡️ Arrow positioning — built-in arrow/caret support
  • ⌨️ Keyboard support — Escape key to close
  • 🖱️ Multiple triggers — click, hover
  • 📏 Auto-update — responds to scroll, resize, and content changes
  • Flicker-freeisReady flag prevents initial position jump
  • 🪶 Lightweight — ~5KB minified + gzipped
  • 📦 Tree-shakeable — only import what you use
  • 🔷 TypeScript — fully typed API

Install

npm install layercraft

Quick Start

import { useAnchor } from 'layercraft';

function Tooltip() {
  const { refCallbacks, floatingStyles, isOpen, isReady } = useAnchor({
    placement: 'top',
    trigger: 'hover',
  });

  return (
    <>
      <button ref={refCallbacks.anchor}>Hover me</button>
      {isOpen && (
        <div 
          ref={refCallbacks.floating} 
          style={{
            ...floatingStyles,
            visibility: isReady ? 'visible' : 'hidden', // Prevents flicker
          }}
        >
          Tooltip content
        </div>
      )}
    </>
  );
}

Placements

        top-start      top      top-end
            ┌───────────────────────┐
            │                       │
 left-start │                       │ right-start
            │                       │
       left │       ANCHOR          │ right
            │                       │
   left-end │                       │ right-end
            │                       │
            └───────────────────────┘
     bottom-start   bottom   bottom-end

Examples

Click Trigger (Popover/Dropdown)

function Dropdown() {
  const { refCallbacks, floatingStyles, isOpen } = useAnchor({
    placement: 'bottom-start',
    trigger: 'click',
    closeOnOutsideClick: true,
    closeOnEscape: true,
  });

  return (
    <>
      <button ref={refCallbacks.anchor}>Open Menu</button>
      {isOpen && (
        <ul ref={refCallbacks.floating} style={floatingStyles}>
          <li>Option 1</li>
          <li>Option 2</li>
          <li>Option 3</li>
        </ul>
      )}
    </>
  );
}

Hover Trigger (Tooltip)

function Tooltip() {
  const { refCallbacks, floatingStyles, isOpen, isReady } = useAnchor({
    placement: 'top',
    trigger: 'hover',
    offset: 6,
  });

  return (
    <>
      <span ref={refCallbacks.anchor}>Hover for info</span>
      {isOpen && (
        <div 
          ref={refCallbacks.floating} 
          style={{
            ...floatingStyles,
            visibility: isReady ? 'visible' : 'hidden',
            background: '#333',
            color: 'white',
            padding: '4px 8px',
            borderRadius: '4px',
            fontSize: '14px',
          }}
        >
          Helpful tooltip text
        </div>
      )}
    </>
  );
}

With Portal (Escape overflow: hidden)

Use Portal when your anchor is inside an overflow: hidden container:

import { useAnchor, Portal } from 'layercraft';

function PopoverWithPortal() {
  const { refCallbacks, floatingStyles, isOpen } = useAnchor({
    placement: 'bottom',
    trigger: 'click',
  });

  return (
    <div style={{ overflow: 'hidden', position: 'relative' }}>
      <button ref={refCallbacks.anchor}>Click me</button>
      {isOpen && (
        <Portal>
          <div ref={refCallbacks.floating} style={floatingStyles}>
            I escape the overflow container!
          </div>
        </Portal>
      )}
    </div>
  );
}

When to use Portal:

  • Anchor is inside overflow: hidden or overflow: auto
  • Anchor is inside a scrollable container
  • Floating element gets clipped by parent boundaries

With Arrow

function TooltipWithArrow() {
  const { refCallbacks, floatingStyles, arrowStyles, isOpen, isReady } = useAnchor({
    placement: 'top',
    trigger: 'hover',
    arrow: { size: 8 },
  });

  return (
    <>
      <button ref={refCallbacks.anchor}>Hover me</button>
      {isOpen && (
        <div 
          ref={refCallbacks.floating} 
          style={{
            ...floatingStyles,
            visibility: isReady ? 'visible' : 'hidden',
            background: '#333',
            color: 'white',
            padding: '8px 12px',
            borderRadius: '4px',
          }}
        >
          Tooltip with arrow
          <div 
            ref={refCallbacks.arrow} 
            style={{
              ...arrowStyles,
              position: 'absolute',
              width: 8,
              height: 8,
              background: '#333',
              transform: `${arrowStyles.transform} rotate(45deg)`,
            }} 
          />
        </div>
      )}
    </>
  );
}

Fallback Placements

Control the flip order when preferred placement doesn't fit:

const { actualPlacement } = useAnchor({
  placement: 'right',
  fallbackPlacements: ['left', 'bottom', 'top'], // Try these in order
});

// actualPlacement tells you which placement was actually used
console.log(`Using: ${actualPlacement}`);

Shift (Prevent Edge Clipping)

Shift keeps the floating element within viewport bounds without changing placement:

const { refCallbacks, floatingStyles, isOpen } = useAnchor({
  placement: 'right',
  shift: true,        // Enabled by default
  shiftPadding: 8,    // Padding from viewport edge
});
Without shift:              With shift:
┌─────────────────┐        ┌─────────────────┐
│            ┌────────     │         ┌──────┐│
│   Anchor   │Tooltip│     │ Anchor  │Tooltip│
│            └────────     │         └──────┘│
└─────────────────┘        └─────────────────┘
    (clips at edge)           (shifted to fit)

To disable shift:

const { ... } = useAnchor({
  placement: 'top',
  shift: false,  // Disable shifting
});

Controlled Mode

Manage open state yourself:

function ControlledPopover() {
  const [isOpen, setIsOpen] = useState(false);
  
  const { refCallbacks, floatingStyles } = useAnchor({
    placement: 'bottom',
    trigger: 'click',
  });

  return (
    <>
      <button 
        ref={refCallbacks.anchor}
        onClick={() => setIsOpen(!isOpen)}
      >
        {isOpen ? 'Close' : 'Open'}
      </button>
      {isOpen && (
        <div ref={refCallbacks.floating} style={floatingStyles}>
          Controlled content
          <button onClick={() => setIsOpen(false)}>Close</button>
        </div>
      )}
    </>
  );
}

Vanilla JavaScript

Use the core functions without React:

import { getPosition, getPositionWithFlip, shiftToViewport, getViewport } from 'layercraft';

const anchor = document.querySelector('#anchor');
const floating = document.querySelector('#floating');

const anchorRect = anchor.getBoundingClientRect();
const floatingRect = floating.getBoundingClientRect();
const viewport = getViewport();

// Step 1: Calculate position with auto-flip
const result = getPositionWithFlip(
  anchorRect,
  floatingRect,
  { placement: 'bottom', offset: 8 },
  viewport
);

// Step 2: Apply shift to keep within viewport
const shifted = shiftToViewport(
  result.top,
  result.left,
  floatingRect.width,
  floatingRect.height,
  viewport,
  8 // padding
);

// Step 3: Apply styles
floating.style.position = 'fixed';
floating.style.top = `${shifted.top}px`;
floating.style.left = `${shifted.left}px`;

API Reference

useAnchor(options)

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | placement | Placement | 'bottom' | Where to position the floating element | | offset | number | 8 | Gap between anchor and floating element (px) | | trigger | 'click' \| 'hover' | 'click' | How to open the floating element | | closeOnOutsideClick | boolean | true | Close when clicking outside | | closeOnEscape | boolean | true | Close when pressing Escape | | autoFlip | boolean | true | Flip to opposite side if no space | | fallbackPlacements | Placement[] | undefined | Custom fallback placement order | | shift | boolean | true | Keep floating element within viewport by shifting | | shiftPadding | number | 8 | Padding from viewport edge when shifting (px) | | arrow | boolean \| { size: number } | false | Enable arrow positioning | | zIndex | number | 9999 | z-index for floating element |

Return Value

| Property | Type | Description | |----------|------|-------------| | isOpen | boolean | Current open state | | setIsOpen | function | Manually control open state | | isReady | boolean | true after position is calculated (use for visibility) | | floatingStyles | CSSProperties | Apply to floating element | | arrowStyles | CSSProperties | Apply to arrow element | | actualPlacement | Placement | The placement used (may differ if flipped) | | refCallbacks.anchor | function | Attach to anchor element | | refCallbacks.floating | function | Attach to floating element | | refCallbacks.arrow | function | Attach to arrow element |

Portal

Renders children into document.body (or custom container) to escape overflow containers.

import { Portal } from 'layercraft';

// Default: renders to document.body
<Portal>
  <div>Rendered at document.body</div>
</Portal>

// Custom container
<Portal container={document.getElementById('portal-root')}>
  <div>Rendered in #portal-root</div>
</Portal>

Core Functions

For vanilla JS or custom implementations:

import { 
  getPosition,           // Basic positioning
  getPositionWithFlip,   // Positioning with auto-flip
  shiftToViewport,       // Keep within viewport by shifting
  getArrowPosition,      // Arrow positioning
  clampToViewport,       // Clamp to viewport bounds
  isOutOfBounds,         // Check if position overflows
  getViewport,           // Get viewport dimensions
} from 'layercraft';

Architecture

src/
├── core/           # Pure positioning math, framework-agnostic
│   ├── getPosition.ts
│   ├── flip.ts
│   └── viewport.ts
└── react/          # React adapter
    ├── useAnchor.ts
    └── Portal.tsx

The core is framework-agnostic. The React hook is a thin wrapper. You could build Vue, Svelte, or vanilla adapters on the same core.

Limitations

  • No z-index coordination — Use the zIndex option to manually manage stacking order between multiple floating elements
  • No animations — Use CSS transitions or animation libraries
  • React 17+ — Uses modern React features

Demo

👉 Live Demo

Local Development

# Install dependencies
npm install

# Start dev server
npm run dev

# Run tests
npm run test

# Build for production
npm run build

# Run Storybook
npm run storybook

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feat/amazing-feature)
  3. Commit your changes (git commit -m 'feat: add amazing feature')
  4. Push to the branch (git push origin feat/amazing-feature)
  5. Open a Pull Request

License

MIT © Venkatesh M