Vision UI

Window

Glass effect with highlights

'use client'

import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
import { XIcon } from 'lucide-react'
import { type HTMLMotionProps, MotionValue, motion, useScroll } from 'motion/react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import React, {
  type RefObject,
  useEffect,
  useId,
  useImperativeHandle,
  useRef,
  useState,
} from 'react'
import { useResizeObserver } from '@/hooks/use-resize-observer'
import { cn } from '@/lib/utils'

type GlassThickness =
  | 'none'
  | 'thinnest'
  | 'thinner'
  | 'thin'
  | 'normal'
  | 'thick'
  | 'thicker'
  | 'thickest'

type WindowControlsProps =
  | boolean
  | {
      /**
       * The URL used when clicking the close button.
       * @default `router.back()`
       */
      href?: string
    }

interface WindowApiProps {
  /**
   * Wrap content in a scroll area.
   *
   * You can use `useWindow` to get the scroll position of the window in the children.
   * @default false
   */
  scroll?: boolean
  /**
   * The thickness of the glass effect.
   * @default "normal"
   */
  thickness?: GlassThickness
  /**
   * The controls to display in the window.
   * @default false
   */
  controls?: WindowControlsProps
  /**
   * The root className for the window wrapper.
   */
  rootClassName?: string
}

interface WindowProps extends HTMLMotionProps<'div'>, WindowApiProps {
  children: React.ReactNode
}

export const getThickness = (thickness: GlassThickness) => {
  switch (thickness) {
    case 'thinnest':
      return 24
    case 'thinner':
      return 32
    case 'thin':
      return 42
    case 'normal':
      return 48
    case 'thick':
      return 64
    case 'thicker':
      return 72
    case 'thickest':
      return 96
    default:
      return 24
  }
}

export const getRings = (thickness: GlassThickness) => {
  switch (thickness) {
    case 'thinnest':
      return '0px 3px 6.5px 0px rgba(0, 0, 0, 0.05), -0.25px 0.35px 0.15px -1.5px rgba(255, 255, 255, 0.15) inset, 0px 0.35px 2px 0px rgba(255, 255, 255, 0.15) inset'
    case 'thinner':
      return '0px 4px 8px 0px rgba(0, 0, 0, 0.05), -0.35px 0.55px 0.25px -1.5px rgba(255, 255, 255, 0.2) inset, 0px 0.55px 2px 0px rgba(255, 255, 255, 0.2) inset'
    case 'thin':
      return '0px 6px 10px 0px rgba(0, 0, 0, 0.05), -0.45px 0.75px 0.35px -1.5px rgba(255, 255, 255, 0.25) inset, 0px 0.75px 2px 0px rgba(255, 255, 255, 0.25) inset'
    case 'normal':
      return '0px 8px 12px 0px rgba(0, 0, 0, 0.05), -0.55px 1px 0.45px -1.5px rgba(255, 255, 255, 0.3) inset, 0px 1px 2px 0px rgba(255, 255, 255, 0.3) inset'
    case 'thick':
      return '0px 12px 16px 0px rgba(0, 0, 0, 0.05), -0.65px 1.25px 0.65px -1.5px rgba(255, 255, 255, 0.35) inset, 0px 1.25px 2px 0px rgba(255, 255, 255, 0.35) inset'
    case 'thicker':
      return '0px 18px 22px 0px rgba(0, 0, 0, 0.05), -0.75px 1.75px 0.75px -1.5px rgba(255, 255, 255, 0.35) inset, 0px 1.75px 6px 0px rgba(255, 255, 255, 0.35) inset'
    case 'thickest':
      return '0px 24px 28px 0px rgba(0, 0, 0, 0.05), -0.85px 1.85px 0.85px -1.5px rgba(255, 255, 255, 0.35) inset, 0px 1.85px 6px 0px rgba(255, 255, 255, 0.35) inset'
    default:
      return '0px 3px 6.5px 0px rgba(0, 0, 0, 0.05), -0.25px 0.35px 0.15px -1.5px rgba(255, 255, 255, 0.15) inset, 0px 0.35px 2px 0px rgba(255, 255, 255, 0.15) inset'
  }
}

export const getHighlightStroke = (thickness: GlassThickness) => {
  switch (thickness) {
    case 'thinnest':
      return '[--mask-stroke:0.75px]'
    case 'thinner':
      return '[--mask-stroke:1px]'
    case 'thin':
      return '[--mask-stroke:1.25px]'
    case 'normal':
      return '[--mask-stroke:1.5px]'
    case 'thick':
      return '[--mask-stroke:1.75px]'
    case 'thicker':
      return '[--mask-stroke:1.85px]'
    case 'thickest':
      return '[--mask-stroke:1.9px]'
    default:
      return '[--mask-stroke:0.75px]'
  }
}

const getHighlightOpacity = (thickness: GlassThickness) => {
  switch (thickness) {
    case 'thinnest':
      return 0.15
    case 'thinner':
      return 0.175
    case 'thin':
      return 0.2
    case 'normal':
      return 0.225
    case 'thick':
      return 0.25
    case 'thicker':
      return 0.275
    case 'thickest':
      return 0.3
    default:
      return 0.15
  }
}

const CONSTANTS = {
  SATURATION: 1.5,
  VAR_RADIUS: '[--radius:34px]',
  VAR_DIAMETER: '[--diameter:68px]',
}

const maskComposite = ['exclude', 'intersect', 'subtract', 'intersect', 'subtract', 'add']

const defaultHighlightStyle = {
  borderRadius: `var(--radius)`,
  maskSize: '100% 100%',
  WebkitMaskSize: '100% 100%',
  maskRepeat: 'no-repeat',
  WebkitMaskRepeat: 'no-repeat',
}

const leftTopHighlight =
  'conic-gradient(from 270deg at var(--radius) var(--radius), transparent 0deg, white 45deg, transparent 170deg), transparent'
const leftTopMaskImage = [
  'linear-gradient(to right, black, black)',
  'linear-gradient(to right, transparent var(--mask-stroke), black calc(var(--mask-stroke) * 2))',
  'linear-gradient(to bottom, transparent var(--mask-stroke), black calc(var(--mask-stroke) * 2))',
  'linear-gradient(to right, black calc(var(--radius) - var(--mask-stroke)), transparent var(--radius))',
  'linear-gradient(to bottom, black calc(var(--radius) - var(--mask-stroke)), transparent var(--radius))',
  'radial-gradient(var(--diameter) var(--diameter) at var(--radius) var(--radius), black var(--mask-inner-distance), transparent var(--mask-outer-distance))',
]
const leftTopHighlightStyle = {
  background: leftTopHighlight,
  maskImage: leftTopMaskImage.join(', '),
  maskComposite: maskComposite.join(', '),
  ...defaultHighlightStyle,
}

const rightBottomHighlight =
  'conic-gradient(from 60deg at var(--radius) var(--radius), transparent 0deg, white 65deg, transparent 160deg), transparent'
const rightBottomMaskImage = [
  'linear-gradient(to left, black, black)',
  'linear-gradient(to left, transparent var(--mask-stroke), black calc(var(--mask-stroke) * 2))',
  'linear-gradient(to top, transparent var(--mask-stroke), black calc(var(--mask-stroke) * 2))',
  'linear-gradient(to left, black calc(var(--radius) - var(--mask-stroke)), transparent var(--radius))',
  'linear-gradient(to top, black calc(var(--radius) - var(--mask-stroke)), transparent var(--radius))',
  'radial-gradient(var(--diameter) var(--diameter) at calc(100% - var(--radius)) calc(100% - var(--radius)), black var(--mask-inner-distance), transparent var(--mask-outer-distance))',
]
const rightBottomHighlightStyle = {
  background: rightBottomHighlight,
  maskImage: rightBottomMaskImage.join(', '),
  maskComposite: maskComposite.join(', '),
  ...defaultHighlightStyle,
}

const WindowContext = React.createContext<{
  scrollY: MotionValue<number>
  width: number | undefined
  height: number | undefined
  visible: boolean
  setVisible: (visible: boolean) => void
  windowId: string
}>({
  scrollY: new MotionValue(0),
  width: 0,
  height: 0,
  visible: false,
  setVisible: () => {},
  windowId: '',
})

const useWindow = () => {
  const context = React.useContext(WindowContext)
  if (!context) {
    throw new Error('useWindowContext must be used within a WindowContext')
  }
  return context
}

const Window = React.forwardRef<HTMLDivElement, WindowProps>(
  (
    {
      children,
      className,
      rootClassName,
      thickness,
      style,
      scroll = false,
      controls = false,
      ...props
    }: WindowProps,
    ref
  ) => {
    const windowId = useId()
    const localRef = useRef<HTMLDivElement>(null)
    const [visible, setVisible] = useState(true)
    // strip out *-h-*, h-* classes classes
    const scrollWindowRegex = /-h-.*|h-.*|^max-h-.*|^min-h-.*|^h-.*|h-.*|max-h-.*|min-h-.*/g
    const scrollWindowClassesName = className?.match(scrollWindowRegex)?.join(' ') || ''
    const restClassesName = className?.replace(scrollWindowRegex, '') || ''
    // get rounded-* classes
    const roundedRegex = /rounded-.*|^rounded/g
    const roundedClassesName = className?.match(roundedRegex)?.join(' ') || ''

    useImperativeHandle(ref, () => localRef.current!)

    const { scrollY } = useScroll({
      container: scroll ? localRef : undefined,
      layoutEffect: false,
    })
    const { width, height } = useResizeObserver({
      ref: localRef as RefObject<HTMLDivElement>,
    })

    // Extract role and aria-label from props if provided
    const { role, 'aria-label': ariaLabel, ...restProps } = props

    return (
      <WindowContext.Provider value={{ scrollY, width, height, visible, setVisible, windowId }}>
        <motion.div
          key={`${windowId}-wrapper`}
          className={cn('relative flex flex-col items-center justify-center', rootClassName)}
          initial={{
            opacity: 1,
            scale: 1,
          }}
          animate={{
            opacity: visible ? 1 : 0,
            scale: visible ? 1 : 0.99,
            transition: {
              duration: 0.4,
            },
          }}
        >
          <motion.div
            className={cn(
              'relative overflow-hidden',
              'before:absolute before:inset-0 before:z-[-1] before:rounded-[var(--radius)]',
              'before:bg-[#80808030]',
              'min-h-[64px] min-w-[64px]',
              CONSTANTS.VAR_DIAMETER,
              CONSTANTS.VAR_RADIUS,
              restClassesName,
              !scroll && scrollWindowClassesName
            )}
            style={{
              backdropFilter:
                thickness === 'none'
                  ? 'none'
                  : `saturate(${CONSTANTS.SATURATION}) blur(${getThickness(thickness || 'normal')}px) brightness(0.85)`,
              WebkitBackdropFilter:
                thickness === 'none'
                  ? 'none'
                  : `saturate(${CONSTANTS.SATURATION}) blur(${getThickness(thickness || 'normal')}px) brightness(0.85)`,
              borderRadius: `var(--radius)`,
              ...style,
            }}
            role={role || 'region'}
            aria-label={ariaLabel || 'Window content'}
            {...restProps}
          >
            {/* HIGHLIGHTRINGS */}
            <motion.div
              className="pointer-events-none absolute inset-x-0 z-40 h-full w-full"
              style={{
                boxShadow: getRings(thickness || 'normal'),
                borderRadius: `var(--radius)`,
                top: 0,
              }}
              aria-hidden="true"
            />
            <motion.div
              className={cn(
                getHighlightStroke(thickness || 'normal'),
                'pointer-events-none absolute inset-[-0.75px] z-40',
                '[--mask-inner-distance:calc(50%-var(--mask-stroke)-var(--mask-stroke))] [--mask-outer-distance:calc(50%-var(--mask-stroke))]'
              )}
              style={{
                ...leftTopHighlightStyle,
                opacity: getHighlightOpacity(thickness || 'normal') + 0.35,
              }}
              aria-hidden="true"
            />
            <motion.div
              className={cn(
                getHighlightStroke(thickness || 'normal'),
                'pointer-events-none absolute inset-[-0.25px] z-40',
                '[--mask-inner-distance:calc(50%-var(--mask-stroke)-var(--mask-stroke))] [--mask-outer-distance:calc(50%-var(--mask-stroke))]'
              )}
              style={{
                ...rightBottomHighlightStyle,
                opacity: getHighlightOpacity(thickness || 'normal') - 0.05,
              }}
              aria-hidden="true"
            />
            {scroll ? (
              <ScrollAreaPrimitive.Root
                className={cn('relative', scrollWindowClassesName)}
                aria-label={ariaLabel ? `Scrollable ${ariaLabel}` : 'Scrollable content'}
              >
                <ScrollAreaPrimitive.Viewport
                  className={cn(
                    'h-full w-full',
                    roundedClassesName.length > 0 ? roundedClassesName : `rounded-[var(--radius)]`,
                    {
                      '!overflow-visible': scrollWindowClassesName.length === 0,
                    }
                  )}
                  ref={localRef}
                  tabIndex={0}
                >
                  {children}
                </ScrollAreaPrimitive.Viewport>
                <ScrollAreaPrimitive.Scrollbar />
                <ScrollAreaPrimitive.Corner />
              </ScrollAreaPrimitive.Root>
            ) : (
              children
            )}
          </motion.div>
          {controls && (
            <WindowControls href={typeof controls === 'object' ? controls.href : undefined} />
          )}
        </motion.div>
      </WindowContext.Provider>
    )
  }
)

Window.displayName = 'Window'

const WindowControls = ({ href }: { href?: string }) => {
  const router = useRouter()
  const buttonRef = useRef<HTMLAnchorElement>(null)
  const { setVisible } = useWindow()

  const onNavigate = (e: { preventDefault: () => void }) => {
    e.preventDefault()
    setVisible(false)
    setTimeout(() => {
      if (href) {
        router.push(href)
      } else {
        router.back()
      }
    }, 450)
  }
  // Handle Escape key press
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Escape') {
        if (document.activeElement === buttonRef.current) {
          // If button is already focused, trigger the action
          onNavigate(e)
        } else {
          // Focus the button on first Escape press
          buttonRef.current?.focus()
        }
      }
    }

    window.addEventListener('keydown', handleKeyDown)
    return () => {
      window.removeEventListener('keydown', handleKeyDown)
    }
  }, [href, router])

  return (
    <motion.div
      className="group/controls absolute inset-x-0 bottom-[var(--window-controls-bottom,-37px)] z-50 mx-auto inline-flex h-[37px] w-[212px] shrink-0 items-center justify-start gap-4 pt-[22px] pr-[28px] pb-px"
      layout
      layoutId="navigation-bar"
      role="toolbar"
      aria-label="Window controls"
    >
      <Link
        href={href || '/'}
        ref={buttonRef}
        onNavigate={onNavigate}
        className={cn(
          'h-[37px] w-[37px]',
          'group/close-btn',
          'peer/close-btn',
          'flex items-center justify-center',
          'focus:outline-none focus-visible:ring-2 focus-visible:ring-white/50 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent',
          'rounded-full'
        )}
        aria-label="Close window"
        title={href ? 'Navigate to previous page' : 'Go back'}
      >
        <span
          className={cn(
            'pointer-events-none',
            'size-3.5 rounded-[100px] bg-white/30 backdrop-blur-[20px]',
            'transition-all duration-300',
            'flex items-center justify-center',
            'group-hover/close-btn:size-6 group-hover/close-btn:bg-white/100',
            'group-active/close-btn:size-4 group-active/close-btn:bg-white/100',
            'group-focus-visible/close-btn:size-6 group-focus-visible/close-btn:bg-white/100'
          )}
        >
          <XIcon className="size-3.5 text-[#333] opacity-0 group-hover/close-btn:size-3 group-hover/close-btn:opacity-100 group-focus-visible/close-btn:size-3 group-focus-visible/close-btn:opacity-100" />
        </span>
      </Link>
      <div
        className={cn(
          'relative h-3.5 w-[136px] rounded-[100px] bg-white/30 backdrop-blur-[20px]',
          'transition-all duration-300',
          'peer-hover/close-btn:ml-[10px] peer-hover/close-btn:w-[126px] peer-hover/close-btn:bg-white/50',
          'peer-focus-visible/close-btn:ml-[10px] peer-focus-visible/close-btn:w-[126px] peer-focus-visible/close-btn:bg-white/50'
        )}
        aria-hidden="true"
      ></div>
    </motion.div>
  )
}

export { Window, WindowControls, useWindow }
export type { WindowProps, WindowApiProps, GlassThickness }

Window requires scroll-area component to be installed.

npx shadcn@latest add scroll-area

Thickness

The Window component has a thickness prop that can be used to set the thickness of the window.

None

Thinnest

Thinner

Thin

Normal

Thick

Thicker

Thickest

import { Window } from '../core/window'

export const WindowExample = () => {
  return (
    <>
      <Window className="flex h-[100px] w-[150px] items-center justify-center" thickness="none">
        <p className="text-white/80">None</p>
      </Window>
      <Window className="flex h-[100px] w-[150px] items-center justify-center" thickness="thinnest">
        <p className="text-white/80">Thinnest</p>
      </Window>
      <Window className="flex h-[100px] w-[150px] items-center justify-center" thickness="thinner">
        <p className="text-white/80">Thinner</p>
      </Window>
      <Window className="flex h-[100px] w-[150px] items-center justify-center" thickness="thin">
        <p className="text-white/80">Thin</p>
      </Window>
      <Window className="flex h-[100px] w-[150px] items-center justify-center" thickness="normal">
        <p className="text-white/80">Normal</p>
      </Window>
      <Window className="flex h-[100px] w-[150px] items-center justify-center" thickness="thick">
        <p className="text-white/80">Thick</p>
      </Window>
      <Window className="flex h-[100px] w-[150px] items-center justify-center" thickness="thicker">
        <p className="text-white/80">Thicker</p>
      </Window>
      <Window className="flex h-[100px] w-[150px] items-center justify-center" thickness="thickest">
        <p className="text-white/80">Thickest</p>
      </Window>
    </>
  )
}

Highlights

You might wonder how the window is able to have a realistic glass effect with highlights. Using css border? Well, this method will only go so far. If you take a closer look at the window, you can see the edges are softened and the highlights has a gradient effect.

This is achieved using a combination of generic css and css image-mask to achieve the effect. Then we use mask-composite to combine the masks. (Similar to layering in Photoshop)

For example, the left top highlight is made with 6 different css gradients.

window.tsx
//...
const maskComposite = [
  "exclude",
  "intersect",
  "subtract",
  "intersect",
  "subtract",
  "add",
];

const defaultHighlightStyle = {
  borderRadius: CONSTANTS.BORDER_RADIUS,
  maskSize: "100% 100%",
  WebkitMaskSize: "100% 100%",
  maskRepeat: "no-repeat",
  WebkitMaskRepeat: "no-repeat",
};

const leftTopHighlight =
  "conic-gradient(from 270deg at var(--radius) var(--radius), transparent 0deg, white 45deg, transparent 170deg), transparent";
const leftTopMaskImage = [
  "linear-gradient(to right, black, black)",
  "linear-gradient(to right, transparent var(--mask-stroke), black calc(var(--mask-stroke) * 2))",
  "linear-gradient(to bottom, transparent var(--mask-stroke), black calc(var(--mask-stroke) * 2))",
  "linear-gradient(to right, black calc(var(--radius) - var(--mask-stroke)), transparent var(--radius))",
  "linear-gradient(to bottom, black calc(var(--radius) - var(--mask-stroke)), transparent var(--radius))",
  "radial-gradient(var(--diameter) var(--diameter) at var(--radius) var(--radius), black var(--mask-inner-distance), transparent var(--mask-outer-distance))",
];
const leftTopHighlightStyle = {
  background: leftTopHighlight,
  maskImage: leftTopMaskImage.join(", "),
  maskComposite: maskComposite.join(", "),
  ...defaultHighlightStyle,
};
//...

<motion.div
  className={cn(
    getHighlightStroke(thickness || "normal"),
    "pointer-events-none absolute inset-[-0.75px] z-40",
    "[--mask-inner-distance:calc(50%-var(--mask-stroke)-var(--mask-stroke))] [--mask-outer-distance:calc(50%-var(--mask-stroke))]",
  )}
  style={{
    ...leftTopHighlightStyle, 
    opacity: getHighlightOpacity(thickness || "normal") + 0.35,
  }}
  aria-hidden="true"
/>;

API

Prop

Type

Edit on GitHub

Last updated on