拟态浮层

popover · neumorphic · 灵感来源:Neumorphism(同色浮起的面板)

点开后人把焦点交给浮层;Esc / 点外关闭,焦点还给触发器

贴边打开时浮层会被夹回视口,不横向溢出

复用卡

Non-modal Popover · 拟态浮层 · 浮层 / Neumorphic

规格 · 术语提示词(给 AI 用)

做一个 Non-modal Popover(拟态浮层):
- 同一份 props(trigger / title / children / placement)
- 同色底 + 大两档外投影,没有边框;投影半径随浮层尺寸放大
- 焦点进出、Esc / 点外关闭与定位夹取同玻璃浮层
用这些 CSS 变量:--color-accent --color-surface --color-text --color-text-muted --dur-fast --ease-standard --glass-border --neuro-bg --neuro-dark --neuro-light
灵感参考:Neumorphism(同色浮起的面板)

参数(CSS 变量)

--color-accent --color-surface --color-text --color-text-muted --dur-fast --ease-standard --glass-border --neuro-bg --neuro-dark --neuro-light

必缺状态

焦点 / 按压

整页场景

(尚未接进整页场景)

安装(一条链接)

调研笔记

拟态浮层

  • 与玻璃浮层同一份 props(trigger / title / children / placement)
  • 同色底 + 大两档的外投影,没有边框;投影半径随浮层尺寸放大,否则读成一块贴纸
  • 焦点进入与归还、Esc / 点外关闭、定位夹取规则与玻璃浮层一致
  • 限制:同色面板叠在同色页面上时,靠投影区分深度;背景有纹理就换极简

实现

popover/neumorphic
import { useEffect, useId, useState, type ReactNode } from 'react';
import { useOverlay } from '~/lib/overlay';
import { Portal, layerStyle, useDismiss, useLayerPosition } from '~/lib/layer';

interface Props {
  /** 触发器文字 */
  trigger: ReactNode;
  /** 浮层标题;也是 dialog 的可访问名 */
  title?: string;
  children: ReactNode;
  placement?: 'top' | 'bottom';
  className?: string;
}

export function NeumorphicPopover({
  trigger,
  title,
  children,
  placement = 'bottom',
  className = '',
}: Props) {
  const id = useId();
  const [open, setOpen] = useState(false);
  const { mounted, entering, leaving } = useOverlay(open);
  const { triggerRef, layerRef, pos } = useLayerPosition(mounted, { placement });
  useDismiss({
    open,
    onClose: () => setOpen(false),
    trigger: triggerRef,
    layer: layerRef,
  });

  // 打开即把焦点交给浮层:Esc 与 Tab 从浮层里开始走,关掉时再还给触发器。
  // 等定位算完(浮层可见)再给焦点,hidden 元素上的 focus() 会被静默吞掉。
  useEffect(() => {
    if (mounted && pos) layerRef.current?.focus();
  }, [mounted, pos]);

  const attachTrigger = (node: HTMLButtonElement | null) => {
    triggerRef.current = node;
  };

  return (
    <>
      <button
        ref={attachTrigger}
        type="button"
        aria-haspopup="dialog"
        aria-expanded={open}
        aria-controls={open ? id : undefined}
        onClick={() => setOpen((v) => !v)}
        className="
          press-feedback px-3.5 py-2 rounded-lg text-xs
          border border-[var(--glass-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)]
          hover:text-[var(--color-text)] transition-colors cursor-pointer
        "
      >
        {trigger}
      </button>

      {mounted && (
        <Portal>
          <div
            ref={layerRef}
            id={id}
            role="dialog"
            aria-label={title ?? '浮层'}
            tabIndex={-1}
            style={layerStyle(pos)}
            className={`
              z-50 w-64 p-4 rounded-xl outline-none
              bg-[var(--neuro-bg)] border border-transparent
              shadow-[6px_6px_18px_var(--neuro-dark),-6px_-6px_18px_var(--neuro-light)]
              transition-[opacity,transform] duration-[var(--dur-fast)] ease-[var(--ease-standard)]
              ${entering || leaving ? 'opacity-0 translate-y-1' : 'opacity-100 translate-y-0'}
              ${className}
            `}
          >
            {title && <h3 className="text-sm font-medium mb-2">{title}</h3>}
            <div className="text-xs text-[var(--color-text-muted)] leading-relaxed">{children}</div>
          </div>
        </Portal>
      )}
    </>
  );
}

元信息

标识
popover/neumorphic
状态
done
最后提交
2026-11-01
风格轴
neumorphic
依赖 token
--color-accent --color-surface --color-text --color-text-muted --dur-fast --ease-standard --glass-border --neuro-bg --neuro-dark --neuro-light
在工作台打开 源码与 notes 都在 src/components/popover/neumorphic/