@arcon.mobi/malic-ui

Vue 3 components for Apple-flavored interface surfaces: Liquid Glass, squircles, and layered Liquid Glass icons.
Install
npm install @arcon.mobi/malic-ui
Latest Version:5.1.0
Vue:3
License:Proprietary
Last Updated:Sep 3, 2026
Links: npm
Maintainer: aj-abad

Malic UI

Vue 3 components for Apple-flavored interface surfaces: refractive Liquid Glass, Figma-smoothed squircles, and layered Liquid Glass icons.

Three families, one package:

Components What it is
Surfaces LiquidGlass, useLiquidGlass An SVG refraction filter over whatever the page has painted behind an element
Shapes Squircle, squirclePath, squircleStyle Continuous-curvature corners, plus the shadow, border and glass a border-radius would have got
Icons LiquidGlassIcon, LiquidGlassLayer, FlatIconLayer, LiquidGlassGroup Declarative SVG composited through WebGL2 into an exportable icon raster

The first two are page chrome — real DOM, real backdrop, live at 60fps. The third is an offline renderer whose output happens to be a canvas. They share a vocabulary and a stylesheet, not a pipeline.

Install

npm install malic-ui vue

Import the package stylesheet once in your application entry:

import 'malic-ui/style.css'

Vue 3.5 or newer is required. The package has no other runtime dependencies — no Tailwind, no VueUse.

Surfaces

Squircle

A rounded rectangle with Figma-style corner smoothing, plus the decoration a rounded rectangle would have got from border-radius, border, box-shadow and backdrop-filter.

<script setup lang="ts">
import { Squircle } from 'malic-ui'
import 'malic-ui/style.css'
</script>

<template>
  <Squircle
    class="p-6"
    :appearance="{ radius: 24, smoothing: 0.8 }"
    glass="md"
    :liquid-glass="true"
  >
    <h2>Refracting the page behind it</h2>
  </Squircle>
</template>
Prop Type Default Description
as string | Component 'div' Root element. Must be single-root and block-level
appearance SquircleAppearance {} Radius, smoothing, border width and border color. Each field accepts a literal or MotionValue-like source
radius / smoothing shape values — 4.x compatibility aliases; the matching appearance field wins
borderWidth / borderColor border values — 4.x compatibility aliases; the matching appearance field wins
surfaceClass string — Classes for the clipped surface layer. Backgrounds go here
clipContent boolean false Clip the slot to the shape too
contentClass string — Classes for the clipped content layer. clipContent only
glass 'sm' | 'md' | 'lg' — Backdrop blur and saturation over a white gradient
liquidGlass boolean | LiquidGlassOptions false Add refraction to the surface
shadow ShadowPreset | layers | null 'glass' with glass Presets sm/md/lg/xl/glass, or raw box-shadow-style layers
highlight boolean | string on with glass Inset hairline; a string is its colour
borderStyle SquircleBorderStyle | SquircleBorderSideStyles 'solid' solid, dashed or dotted; the tuple is [top, right, bottom, left] and also accepts none
rightInset number | MotionValueLike<number> 0 Move the right edge in without changing layout width

Exposes rebuild(), which regenerates the refractive maps — see Rebuilding after a transform.

Why it is layered rather than one clipped div

clip-path clips the element's entire subtree, and CSS decoration is shaped by border-radius, which knows nothing about the path. So none of it survives being put on a clipped box: an outer box-shadow paints outside the border box and is clipped away entirely, a CSS border is shaped like the rectangle and gets sliced diagonally at each corner, and any popover a child renders inline is cut off at the edge.

The two obvious repairs both fail. Wrapping in a parent with filter: drop-shadow() shadows the clipped silhouette correctly, but a filter establishes a Backdrop Root — so backdrop-filter on anything inside it samples an empty backdrop and the glass turns flat; shadow would cost glassmorphism. And blurring a clipped caster does nothing, because a CSS filter is applied before clip-path: the blur is cut back to a hard edge.

So decoration is split into sibling layers, each in whichever technology shapes it correctly, and only the surface is clipped:

Layer What it is Why there
shadow blurred SVG paths, behind outside the box, and a sibling — no ancestor filter, so glass survives
surface clipped div or LiquidGlass the one place backdrop-filter can be clipped to the shape
strokes stroked SVG paths, in front a stroke follows the real path; CSS borders can't
content the slot, unclipped, in flow so children can render menus past the edge

Three consequences worth knowing before the first surprise:

  • Backgrounds go on surfaceClass, not class. class lands on the unclipped root, so a background there paints a plain rectangle behind the squircle and its corners stick out.
  • Fade a glass squircle with --squircle-opacity, not opacity. An opacity below 1 anywhere above the surface is a Backdrop Root, so the glass reads flat for the whole animation and the blur snaps in on the frame it lands. The variable fades each layer instead of the group, so the surface keeps its own live backdrop; it inherits, so it can go on whatever element is actually being animated. Use --squircle-group-opacity on a container fading several at once — the two multiply, where a container reusing the inner name would just be shadowed by it. A squircle with no glass has no backdrop to lose and can be faded either way.
  • Don't put a CSS border on the root. The surface is sized to the padding box, so a border would offset it. Use borderWidth.

Content is not clipped by default; padding keeps it inside. clipContent opts in, for content that paints to the edge anyway — a map canvas, a full-bleed image. It moves the slot into its own clipped layer between surface and strokes rather than clipping the root, so the shadow still survives; the content layer is then absolutely positioned, which means the root has to be sized by layout and padding on the root no longer insets it.

Animated geometry and appearance

appearance.radius, appearance.smoothing, appearance.borderWidth and appearance.borderColor accept either their literal value or any object with Motion's read-only get()/on('change') interface. The decorative layers own those subscriptions in a separate render boundary, so animation frames redraw the SVG geometry without evaluating the default slot. malic-ui does not depend on Motion or own the transition. rightInset accepts the same kind of source outside appearance, since it changes the visible viewport rather than the surface's style.

<script setup lang="ts">
import { animate, useMotionValue, useTransform } from 'motion-v'
import { Squircle, type SquircleAppearance } from 'malic-ui'

const progress = useMotionValue(0)
const appearance: SquircleAppearance = {
  radius: useTransform(progress, [0, 1], [12, 28]),
  smoothing: useTransform(progress, [0, 1], [0.4, 0.8]),
  borderWidth: useTransform(progress, [0, 1], [1, 3]),
  borderColor: useTransform(
    progress,
    [0, 1],
    ['rgba(0, 0, 0, 0.08)', 'rgba(0, 129, 104, 0.8)'],
  ),
}

const expand = () => animate(progress, 1, { type: 'spring', stiffness: 500, damping: 40 })
</script>

<template>
  <Squircle :appearance="appearance" @click="expand">…</Squircle>
</template>

Keep the appearance object stable and pass Motion values themselves. Mirroring their latest value into Vue state would put the parent back on the per-frame render path. Animated radii should publish numeric pixel values; radius tokens remain useful as static appearance values. With refractive liquidGlass, the SVG silhouette follows the animation live and the expensive refractive maps rebuild after the shape has been still for 150 ms.

Border style, width and color may each use one value around the whole outline or four values in CSS order. Scalar properties repeat on every side. Each corner's miter follows the adjacent widths so their inner edges meet without a notch; if one side is omitted with none or a zero tuple width, its neighbor extends all the way through that corner:

<Squircle
  :appearance="{
    borderWidth: [2, 4, 6, 0],
    borderColor: ['red', 'blue', 'green', 'transparent'],
  }"
  :border-style="['solid', 'dashed', 'dotted', 'none']"
/>
Squircles without the component

squircleStyle returns a cached clip-path for a squircle whose size is known at author time — an icon chip, a swatch — skipping the ResizeObserver, the second render pass measuring forces, and the per-instance <clipPath>:

import { squircleStyle } from 'malic-ui'

const chip = squircleStyle({ width: 40, height: 40, radius: 'lg' })
// → { clipPath: 'path("M …")' }

It does clip children, which the component deliberately does not, so keep it to surfaces whose content stays inside. squirclePath({ width, height, radius, smoothing, x?, y? }) returns the raw d if you want to draw the shape yourself; radius may be one px value or [topLeft, topRight, bottomRight, bottomLeft]. Every command after the opening M is relative, so x/y let several shapes share one user space. radiusToPx, radiiToPx and RADIUS_TOKENS resolve Tailwind's rounded-* keys.

LiquidGlass

A refractive glass surface over whatever the page has painted behind it. The element keeps its own background, padding and children; all this adds is a generated SVG filter in backdrop-filter, so the backdrop bends through a bezel that follows the element's silhouette.

<script setup lang="ts">
import { LiquidGlass } from 'malic-ui'
</script>

<template>
  <LiquidGlass
    class="glass-nav"
    :options="{ bezelWidth: 20, glassThickness: 1, refractiveIndex: 1.5 }"
  >
    <p>Your content here</p>
  </LiquidGlass>
</template>

<style scoped>
.glass-nav {
  border-radius: 9999px;
  padding: 1rem;
  background: rgb(255 255 255 / 0.5);
  /* Fallback for everything that is not Chromium. */
  backdrop-filter: blur(1rem);
}
</style>
Prop Type Default Description
options LiquidGlassOptions {} See below
tag string 'div' Element to render
disabled boolean false Same as options.disabled; either source turns it off
Option Type Default Description
bezelWidth number 20 Width of the refractive bezel in px
glassThickness number 0.8 Thickness of the virtual glass surface
refractiveIndex number 1.5 Refractive index — glass is 1.5, water 1.33
specularOpacity number 0.4 Specular rim highlight opacity, 0–1
specularAngle number -60 Light source angle in degrees
specularPower number 6 Specular sharpness exponent
blurAmount number 0.5 Gaussian blur in px after refraction
shape GlassShape { type: 'rounded-rect' } Silhouette to build the bezel around
disabled boolean false Turn the effect off entirely

Exposes rebuild().

Shape: the bezel has to match the clip

The bezel is drawn per pixel from how far that pixel is from the silhouette edge and which way the edge faces, so it only lines up if it is built from the same shape the element is clipped to. Get that wrong and the error shows exactly where a corner is: the bezel bulges past the clip on the diagonal and leaves a flat, unrefracted band along each side.

type GlassShape =
  // A rounded rectangle. Omit `radius` to read the element's own border-radius.
  | { type: 'rounded-rect'; radius?: number }
  // A Figma-smoothed squircle, matching `Squircle` and `squircleStyle`.
  | { type: 'squircle'; radius?: number | RadiusToken; smoothing?: number }
  // Any outline, in the element's user space with the origin top-left.
  | { type: 'path'; d: string }

A rounded-rect has a closed-form distance field and is the fast path — it is also the default, read from the element's computed border-radius (percentages resolved against the box, clamped to a pill). A squircle or a path has no closed form, so the outline is flattened and every pixel inside the bezel gets its exact distance to the nearest segment; the cost scales with perimeter × bezel width rather than with area.

Squircle wires this up for you. It hands its surface the very d it is clipping with, so the two can never drift:

<!-- The bezel refracts around the smoothed corner, not a circular one. -->
<Squircle :appearance="{ radius: 32, smoothing: 0.8 }" :liquid-glass="true">…</Squircle>

You only need shape by hand when you clip an element yourself.

Where the glass stops

Chromium clips a backdrop-filter to the element's border box and its border-radius, and to nothing else. A clip-path is not consulted — not on the element, not on an ancestor — and a plain blur() spills exactly as far as a filter reference does. So a squircle-clipped surface would paint its filtered backdrop right out to the corners of its bounding rectangle, outside the shape, where the surface has no background of its own to cover it.

The filter therefore stops itself: a third generated image holds the silhouette, and the whole chain is composited in it. Where the output is transparent the unfiltered backdrop shows through, which is what the clip would have given. It works for any silhouette rather than the one shape CSS can describe, and it costs one more image per rebuild.

Nothing about this needs configuring — the mask comes from the same distance field as the other two maps, so it always agrees with the bezel.

Turning it off

disabled takes the effect off the element without unmounting or restyling it: nothing is generated, nothing is observed, and no inline backdrop-filter is written, so the element falls back to whatever CSS it carries. Bind it to a breakpoint or a preference rather than rendering a second copy of the element or beating the inline style with !important.

<script setup>
import { useMediaQuery } from '@vueuse/core'

const isDesktop = useMediaQuery('(min-width: 768px)')
</script>

<template>
  <LiquidGlass :disabled="!isDesktop" class="card">…</LiquidGlass>
</template>

Toggling it back on regenerates the maps at the size the element has by then — but the last generated pair is cached, so if nothing that feeds them has changed (size, shape, or any option) it is reused rather than redrawn. Generating them is a per-pixel pass plus two PNG encodes, which is what makes that cache the thing that lets disabled be driven from something as hot as a scroll threshold:

<script setup>
import { useWindowScroll } from '@vueuse/core'

const { y } = useWindowScroll()
const atTop = computed(() => y.value < 10)
</script>

<template>
  <!-- Bare over the hero, glass once the page moves. -->
  <LiquidGlass :disabled="atTop" class="nav">…</LiquidGlass>
</template>
Rebuilding after a transform

The element is watched with a ResizeObserver, which only fires on layout changes — while the size the maps are built from is the rendered one. An element whose rendered size is driven by a transform (a reveal animation that scales it in, say) therefore has to say when it has settled. Squircle forwards the same method.

<script setup>
import { useTemplateRef } from 'vue'

const glass = useTemplateRef('glass')
</script>

<template>
  <motion.div
    :initial="{ scale: 0.86 }"
    :animate="{ scale: 1 }"
    @animation-complete="glass?.rebuild()"
  >
    <LiquidGlass ref="glass">…</LiquidGlass>
  </motion.div>
</template>
useLiquidGlass

For custom layouts, drive the composable directly and wire up the SVG filter and backdrop-filter yourself. It accepts a plain object, a ref, or a getter — pass one of the latter two if any option has to change after mount:

import { useLiquidGlass } from 'malic-ui'

const {
  filterId, glassRef, dispUrl, specUrl, scale, filterWidth, filterHeight,
  isReady, isSupported, blurAmount, isDisabled, rebuild,
} = useLiquidGlass(() => ({ bezelWidth: 20, disabled: !isDesktop.value }))
Browser support

SVG filter references in backdrop-filter are Chromium-only. Support is detected once at mount from two signals, because neither is enough alone: the property has to parse a url() reference (which rules out Safari 17 and earlier, where only -webkit-backdrop-filter ever existed), and the engine has to be Chromium (which rules out Firefox, where the reference parses and is then ignored). Client hints answer the second question where they exist, with window.chrome and the user agent as fallbacks — window.chrome alone is not enough, since plain Chromium builds don't expose it and support the effect perfectly well.

Everywhere the answer is no, nothing is generated and no inline filter is written, so the element keeps whatever CSS fallback it carries. Give every glass surface a plain backdrop-filter: blur() in CSS; Squircle's glass presets already do.

Liquid Glass icons

Consumers author normal SVG geometry inside FlatIconLayer and LiquidGlassLayer. Malic UI rasterizes those live sources, composites the icon through WebGL2 when available, and automatically falls back to Canvas 2D. The visible result is a canvas-backed raster image and can be exported as PNG.

Basic usage

<script setup lang="ts">
import {
  FlatIconLayer,
  LiquidGlassIcon,
  LiquidGlassLayer,
} from 'malic-ui'
import 'malic-ui/style.css'
</script>

<template>
  <LiquidGlassIcon
    view-box="0 0 256 256"
    :size="256"
    :resolution="1024"
    label="Add"
    :surface="{
      type: 'linear-gradient',
      angle: 90,
      stops: [
        { offset: 0, color: '#ff6b6b' },
        { offset: 1, color: '#5f67ff' },
      ],
    }"
  >
    <template #default>
      <FlatIconLayer :opacity="0.35">
        <template #default>
          <circle cx="128" cy="128" r="96" fill="#ffffff" />
        </template>
      </FlatIconLayer>

      <LiquidGlassLayer
        :material="{
          blur: 0.35,
          frost: 0.25,
          translucency: 0.46,
          specular: 0.8,
          shadow: { opacity: 0.5, kind: 'layer-color' },
        }"
      >
        <template #default>
          <path d="M224 128a8 8 0 0 1-8 8h-80v80a8 8 0 0 1-16 0v-80H40a8 8 0 0 1 0-16h80V40a8 8 0 0 1 16 0v80h80a8 8 0 0 1 8 8Z" />
        </template>
      </LiquidGlassLayer>
    </template>
  </LiquidGlassIcon>
</template>

Shared SVG definitions

Put gradients, masks, filters, and clip paths used by more than one layer in the root defs slot. IDs and all local references are scoped per icon and layer during rasterization.

<LiquidGlassIcon :surface="surface">
  <template #defs>
    <linearGradient id="shared-paint">
      <stop offset="0" stop-color="#ffffff" />
      <stop offset="1" stop-color="#8ba7ff" />
    </linearGradient>
  </template>

  <template #default>
    <FlatIconLayer>
      <template #default>
        <circle cx="128" cy="128" r="80" fill="url(#shared-paint)" />
      </template>
    </FlatIconLayer>
  </template>
</LiquidGlassIcon>

Export

The root component exposes rendering and canvas export methods. Exports wait for the newest requested reactive frame before reading the canvas.

<script setup lang="ts">
import { ref } from 'vue'
import type { LiquidGlassIconExpose } from 'malic-ui'

const icon = ref<LiquidGlassIconExpose | null>(null)

async function download() {
  const blob = await icon.value?.toBlob('image/png')
  if (!blob) return

  const url = URL.createObjectURL(blob)
  const anchor = document.createElement('a')
  anchor.href = url
  anchor.download = 'icon.png'
  anchor.click()
  URL.revokeObjectURL(url)
}
</script>

toDataURL(type?, quality?), render(), and getCanvas() are also available.

LiquidGlassIcon

Props
Prop Type Default
surface SurfacePaint Required
viewBox string "0 0 256 256"
size number | string 256
resolution number 1024, clamped to 64…4096
appearance IconAppearance (six renditions; see below) 'default'
tint { color: string; strength?: number } Purple at 0.65
colorSpace 'srgb' | 'display-p3' 'srgb'
lightAngle number 135 degrees
label string Decorative when omitted
specularPolicy 'all' | 'topmost' 'all'

Number size values are CSS pixels. String values such as "12rem" pass through to the wrapper.

IconAppearance is 'default' | 'dark' | 'clear-light' | 'clear-dark' | 'tinted-light' | 'tinted-dark'. The legacy 'clear' spelling remains accepted as an alias for 'clear-light'. Clear renditions retain refraction, frost, and edge light while contributing no opaque gray surface; tinted renditions use the root tint. Use colorSpace="display-p3" with CSS colors such as color(display-p3 1 0.25 0.1) for wide-gamut rasterization and compositing.

Events
Event Meaning
ready First successful frame committed
rendered A current frame committed, with backend, duration, and resolution
backend-change WebGL activation, fallback, loss, or restoration
error Typed validation, rasterization, WebGL, Canvas, or export failure

The last successful frame remains visible if a newer frame fails.

LiquidGlassGroup

Wrap layers in LiquidGlassGroup to treat them as one glass object, mirroring Icon Composer's groups.

<LiquidGlassGroup :material="{ translucency: 0.5, specular: 1 }" lighting="combined">
  <LiquidGlassLayer><template #default><circle cx="100" cy="128" r="60" /></template></LiquidGlassLayer>
  <LiquidGlassLayer><template #default><circle cx="156" cy="128" r="60" /></template></LiquidGlassLayer>
</LiquidGlassGroup>
Prop Type Default
id string SSR-stable generated ID
material GlassMaterial Package default material
lighting 'combined' | 'individual' 'combined'
opacity number 1
blend BlendMode 'normal'

With combined lighting, glass-enabled member layers are composited together and the group material is applied once to the union silhouette — one rim highlight, one shadow, and no seams where shapes overlap. With individual, each glass member receives its own lighting pass. The material always belongs to the group, matching the Icon Composer schema; member material values inside a group are ignored. Group opacity and blend are applied once to the finished group, while flat members remain ordinary artwork. Groups cannot be nested, IDs must be unique, members must occupy one contiguous z-order block, and an icon may contain at most four groups.

Layer props

Both layer components accept:

Prop Type Default
id string SSR-stable generated ID
position { x: number; y: number } View-box center
scale number 1
opacity number 1, clamped to 0…1
blend BlendMode 'normal'
fill LayerPaint { type: 'source' }
visible boolean true
appearances LayerAppearanceOverrides None

LiquidGlassLayer additionally accepts material: GlassMaterial. FlatIconLayer deliberately has no material prop and bypasses all glass passes.

Positions use root view-box units. Scale is centered on the full view box. SVG transform attributes inside the slot remain part of the authored geometry.

appearances specializes fill, opacity, blend, or hidden for any of the six renditions without duplicating geometry:

<LiquidGlassLayer
  :appearances="{
    dark: { opacity: 0.75 },
    'tinted-light': { fill: { type: 'solid', color: '#fff' }, blend: 'screen' },
    'clear-dark': { hidden: true },
  }"
/>

Paint and material types

type SurfacePaint =
  | { type: 'solid'; color: string }
  | { type: 'linear-gradient'; angle?: number; stops: readonly ColorStop[] }

type LayerPaint =
  | { type: 'source' }
  | { type: 'solid'; color: string }
  | { type: 'linear-gradient'; angle?: number; stops: readonly ColorStop[] }

interface GlassMaterial {
  blur?: number
  frost?: number
  translucency?: number
  specular?: false | number
  shadow?: false | {
    opacity?: number
    kind?: 'neutral' | 'layer-color' | 'none'
  }
  darkening?: number
  monochrome?: number
}

Material values use the 0…1 range. blur controls backdrop diffusion while frost independently controls optical scattering/milkiness, allowing the parity harness to calibrate each response without conflating them. Defaults are blur: 0.25, frost: 0.25, translucency: 0.5, specular: 1, and a neutral shadow at 0.5. neutral is a soft black drop, layer-color blurs the artwork's own per-pixel colors onto the backdrop, and none disables the shadow. The deprecated { strength, type: 'chromatic' } form remains accepted and maps to { opacity, kind: 'layer-color' }. Invalid numeric values are normalized with development warnings. Invalid paint definitions, duplicate layer/group IDs, missing parent context, and unsafe SVG resources surface typed errors.

SVG authoring rules

  • Supply geometry such as <path>, <circle>, and <g>—not a nested root <svg>.
  • Prefer SVG presentation attributes or inline styles. Common inherited presentation properties and currentColor are resolved for standalone rasterization.
  • Shared definitions belong in the root defs slot. Layer-local <defs> may stay inside a layer.
  • Reactive attributes, nested children, v-if, v-for, and keyed moves are observed automatically. Source DOM order is the z-order.
  • External <image> sources must be same-origin, data:, or blob: URLs. External <use> references are rejected.
  • Empty layers render transparently.

Rendering behavior

Malic UI first attempts WebGL2 and uses a bounded shared renderer pool across component instances. Jobs sharing a resolution are serialized and copied into instance-owned scratch canvases before the lease is released. If WebGL2 is unavailable or its context is lost, rendering continues through Canvas 2D.

The WebGL2 glass material builds a signed distance field with a GPU jump-flood pass. The nearest silhouette coordinate drives a scale-independent spherical edge profile: the backdrop refracts through it with slight chromatic dispersion, a rim highlight follows the edges facing the light, and a weaker highlight appears opposite. This remains stable at corners, thin shapes, disconnected geometry, and production resolution. Backdrop blur uses a bounded dual-Kawase downsample/upsample pyramid, avoiding stretched sampling taps and banding at high blur values; its radius mapping lives in blurPyramid.ts for harness calibration. Blur and compositing run in linear light, and frost is applied as a separate scattering response. Artwork colors are otherwise passed through faithfully. The Canvas 2D fallback preserves the same composition, frost, shadow, and edge-lighting model with a native filter blur and no chromatic refraction.

The declared surface is the backdrop sampled by the glass pipeline. The component does not sample arbitrary HTML or imagery behind it on the page. In clear renditions it is used only for glass sampling and dimming; the exported icon remains transparent outside its authored glass/content coverage.

The composition is full-bleed: the surface and layers cover the whole square canvas, and the continuous-curvature squircle mask (nominal corner radius 25.8% of the icon size, matching the iOS 26 grid) is applied by a final icon-edge glass pass that refracts and highlights the composition's rim. Exports are transparent outside the mask and contain no baked outer drop shadow — apply page shadows in CSS if desired.

Although layer authoring uses SVG, the final visible output is a canvas/raster image. This keeps glass compositing and deterministic PNG export consistent across arbitrary SVG sources.

SSR

Package imports and server rendering do not access window, document, Image, canvas, ResizeObserver, or WebGL. IDs come from Vue's SSR-stable useId(), so a filter or clip path the server printed is the one the client hydrates onto.

Icons begin rendering after client mount; the server output contains the wrapper, a placeholder canvas, and the hidden SVG source tree. Surfaces render their real markup from the first byte — a squircle's clipped surface layer is there, falling back to a plain border-radius until layout has a box to measure, and its shadow and stroke layers, which have nothing to draw without one, are simply absent.

Browser support

  • Modern browsers with Canvas 2D, SVG serialization, MutationObserver, and Blob URLs.
  • WebGL2 is optional but recommended for icons; Canvas 2D is the fallback.
  • createImageBitmap is preferred when available; an HTMLImageElement decoder is used otherwise.
  • Squircle works everywhere clip-path does. Only the refractive LiquidGlass layer is Chromium-only — see Browser support under surfaces.

Development

npm install
npm run dev        # Storybook on :1009
npm run verify

npm run dev opens the Storybook, which is both the component documentation and the fixture surface the Chromium suite drives — stories under Surfaces, Icons and Shapes, plus an Internal section holding the two fixtures that exist only for the tests.

Because the specs navigate to story iframes rather than to a page written for them, a demo and the test that guards it cannot drift. Surfaces → LiquidGlass → Bezel follows the clip renders the same squircle twice — once with the bezel built from its outline, once from a rounded rectangle of the same radius — and tests/browser/surface.spec.ts reads both displacement maps back out of it to check the bezel lands inside the clip.

npm run verify runs strict Vue/TypeScript checking, unit and SSR tests, the Chromium suite, the library and Storybook builds, and the packed-package consumer build.

Related Packages

@surdeddd/liquidglass

Liquid Glass for the whole web in one package: core engine plus /react, /vue, /s...

2,374 3
@ozcanyldzhn/liquid-glass-js

Framework-agnostic iOS 26 Liquid Glass effect Web Components for Vue, Nuxt, Reac...

669 5
@mizuui/liquid-glass-vue

A component for Vue 3. Anything inside the tag sits on real refracting glass.

834
@daisigu/vue-liquid-glass

A beautiful Vue 3 component that creates a stunning liquid glass morphism effect...

1,141 4
@agou/liquid-glass-vue

Vue 3 adapter for @agou/liquid-glass-core

296

Version History

Version Released Vue License
5.1.0 >=3.5.0 <4 Proprietary
5.0.0 >=3.5.0 <4 Proprietary
4.3.0 >=3.5.0 <4 Proprietary
4.2.0 >=3.5.0 <4 Proprietary
4.1.0 >=3.5.0 <4 Proprietary