Lisse

repository·main·Indexed 42 days ago

https://github.com/jacethings/lisse

A library for generating smooth-corner SVG primitives and CSS clip-paths, enabling high-fidelity corner curves like Figma's 'squircle' in web applications. Includes a core framework-agnostic package (@lisse/core) for generating path data and clip-paths, as well as a browser extension and userscript for applying smoothing to existing websites.

Tokens
59.5K
Snippets
198
Records
269
Agent score
53%

What's inside Lisse

  1. Overview of Lisse corner curve types

    main

    Lisse provides four distinct corner curve algorithms. You can choose between them to achieve different aesthetic results, ranging from standard CSS behavior to high-end design curves like Figma's squircle.

    • arc: A standard quarter circle, identical to the CSS border-radius behavior.
    • squircle (default): Implements Figma's corner smoothing algorithm using cubic shoulders and a central arc.
    • superellipse: A mathematical superellipse defined by |x/R|^n + |y/R|^n = 1. It provides G2 continuity with edges when n > 2.
    • clothoid: An Euler-spiral blend that transitions from a straight edge to a central arc, providing G2 continuity everywhere.
  2. How resize handling and anchor positioning work

    main

    Resize Handling

    Lisse uses a single shared ResizeObserver for all instances. To optimize performance, callbacks are batched via requestAnimationFrame, ensuring that multiple resizing elements only trigger one re-render pass per frame. The observer disconnects automatically when the last observed element is removed.

    Anchor Positioning

    SVG overlays (borders and shadows) are absolutely positioned inside an anchor element.

    • Lisse automatically sets position: relative on the anchor if it is currently position: static.
    • A ref-counting system is used so that if multiple Lisse instances share the same anchor, the position is only reset to static when the final instance unmounts.
  3. Understand what `@lisse` benchmarks measure

    main

    The benchmark suite measures JS-only cost under varying instance counts and effect configurations. It uses happy-dom to simulate a DOM environment without a full rendering pipeline.

    Measured Hot Paths

    Each adapter benchmark (React, Vue, Svelte, Octane) drives three specific hot paths:

    • Resize: Simulates a ResizeObserver callback with changing dimensions (alternating between 200x100 and 240x120) to ensure the library performs actual re-syncs rather than bailing on unchanged sizes.
    • Mount: Measures the cost of rendering N <SmoothCorners /> instances from scratch, including the initial clip-path application and SVG overlay.
    • Update: Measures the cost of mutating the corners.radius prop on every instance, covering the framework commit and the adapter's layout-effect equivalent.

    Benchmark Dimensions

    The suite exercises a grid of 16 cases per adapter:

    • Instance counts: 1, 10, 50, 100
    • autoEffects: true, false
    • Effects present: none, innerBorder: { width, color, opacity }
  4. Configure Gradients for Borders

    main

    Lisse supports linear and radial gradients for border colors via the GradientConfig type. This can be passed to the color property of a BorderConfig.

    Linear Gradient

    • type: Must be "linear".
    • angle: CSS degrees (e.g., 0 for bottom-to-top, 90 for left-to-right). Defaults to 0.
    • stops: An array of GradientStop objects.

    Radial Gradient

    • type: Must be "radial".
    • cx, cy, r: Center X, Center Y, and Radius (0-1 range). Defaults to 0.5.
    • stops: An array of GradientStop objects.

    GradientStop

    • offset: Position from 0 to 1.
    • color: Hex color string (3 or 6 digits).
    • opacity: Opacity from 0 to 1 (defaults to 1).
    const gradient: GradientConfig = {
      type: "linear",
      angle: 45,
      stops: [
        { offset: 0, color: "#ff0000" },
        { offset: 1, color: "#0000ff" }
      ]
    };
  5. Select and compare curve types in Lisse

    main

    Lisse provides different mathematical constructions for corner curves, which can be distinguished by their curvature continuity (G1 vs G2). When choosing a curve type, consider the following options:

    1. Arc: A standard circular arc (G1 continuity with a hard curvature jump).
    2. Squircle: The current Lisse default (G1 continuity with a smaller curvature jump).
    3. Superellipse: A G2 curve defined by an exponent (useful for matching the CSS corner-shape: squircle aesthetic).
    4. Clothoid blend: A G2 curve that preserves a 'Figma-like' feel by using a clothoid (Euler-spiral) to replace the central arc. This provides a continuous curvature comb and is the recommended option for high-quality smoothing.
    5. Apple replica: A G2 curve using exact iOS constants to replicate Apple's specific corner asymmetry.

    For most high-end UI needs, the clothoid blend is the preferred choice as it removes the curvature discontinuity found in G1 curves while maintaining the macroscopic shape.

    /* Conceptual mapping of curve options */
    {
      curve: 'squircle' | 'g2' | 'superellipse' | 'apple'
    }
  6. Ordering multiple shadows

    main

    When providing an array of shadows, the rendering order follows the CSS convention: the first shadow in the array is rendered on top (closest to the element).

    Internally, Lisse renders shadows in reverse order in the SVG DOM so that the SVG 'later paints on top' rule correctly implements the 'first listed is topmost' rule.

  7. Use built-in SVG effects in Lisse

    main

    Lisse uses clip-path to create squircle shapes, which cuts through standard CSS borders and shadows. To maintain visual fidelity, Lisse provides five built-in SVG-based effects that follow the squircle shape perfectly. These effects can be passed as props to the component or used via hooks/composables.

    Available Effects:

    • innerBorder: Border drawn inside the squircle path (clipped to the shape).
    • outerBorder: Border drawn outside the squircle path (masked to the exterior).
    • middleBorder: Border centered on the squircle path (half inside, half outside).
    • innerShadow: Inset shadow inside the squircle.
    • shadow: Drop shadow behind the squircle.

    Note that middleBorder and outerBorder have no CSS equivalents and must be provided via explicit props.

    <SmoothCorners
      corners={{ radius: 24 }}
      innerBorder={{ width: 1, color: "#ffffff", opacity: 0.2 }}
      outerBorder={{ width: 2, color: "#000000", opacity: 0.1 }}
      middleBorder={{ width: 1, color: "#ff0000", opacity: 0.5 }}
      innerShadow={{ offsetX: 0, offsetY: 2, blur: 4, spread: 0, color: "#000000", opacity: 0.15 }}
      shadow={{ offsetX: 0, offsetY: 8, blur: 24, spread: 0, color: "#000000", opacity: 0.2 }}
      style={{ background: "linear-gradient(135deg, #667eea, #764ba2)", padding: 32 }}
    >
      <p style={{ color: "#fff" }}>Card with all effects</p>
    </SmoothCorners>
  8. How Auto Effects work in @lisse/vue

    main

    Lisse uses clip-path to create smooth corners, which normally cuts off CSS border and box-shadow styles. To solve this, Lisse features Auto Effects (enabled by default).

    When an element is mounted, Lisse automatically:

    1. Reads the computed border and box-shadow from the element.
    2. Converts them into equivalent SVG effects (innerBorder, shadow, or innerShadow).
    3. Strips the original CSS properties to prevent clipping artifacts.
    4. Restores the original CSS on unmount.

    Note on Wrapper Divs: The SmoothCorners component renders a wrapper <div> with position: relative when effects are active. If this interferes with your layout (e.g., Flexbox or Grid), use the useSmoothCorners composable instead, which applies effects directly to the target element without a wrapper.

    <!-- The CSS border is automatically converted to an SVG inner border -->
    <template>
      <SmoothCorners :corners="{ radius: 24 }" style="border: 2px solid red; padding: 24px">
        Content with auto border
      </SmoothCorners>
    </template>
  9. Understand the auto-effects contract (mount-time only)

    main

    The extractAndStripEffects function reads computed border and shadow values only once at mount.

    Important: It does not re-extract values when:

    • Theme tokens or CSS variables are updated.
    • Dark mode is toggled.
    • Ancestor classes change.
    • Media queries transition.

    If a consumer needs to update effects due to runtime theme changes, they must currently re-mount the affected elements. An imperative refresh() API is planned for v0.4.

  10. Compare Lisse smoothing values against Apple SwiftUI

    main

    Lisse's squircle geometry can be tuned using different smoothing parameters to match specific design tools or Apple's implementation. Based on geometric error analysis (Hausdorff error) against SwiftUI .continuous at 400×400 R=100:

    Smoothing ParameterTargetHausdorff Error
    0.60 (FIGMA_SMOOTHING)Figma 60% preset~0.58 px
    0.65 (APPLE_SMOOTHING)Lisse default~0.52 px
    ~0.636Numeric best fit~0.51 px

    Key Takeaways:

    • Lisse defaults to APPLE_SMOOTHING (0.65).
    • Use FIGMA_SMOOTHING (0.60) to match Figma's labeled 60% preset.
    • A residual error of ≈ 0.5 px is expected because the underlying mathematical constructions differ between Lisse and Apple's implementation.
  11. How Auto Effects work in @lisse/react

    main

    Lisse uses CSS clip-path to create squircle shapes, which normally cuts through standard CSS border and box-shadow properties. To solve this, the library features Auto Effects (enabled by default).

    When enabled, the library automatically:

    1. Reads the element's computed border and box-shadow on mount.
    2. Converts them into equivalent SVG effects (innerBorder, shadow, or innerShadow).
    3. Strips the original CSS properties to prevent clipping artifacts.
    4. Restores the original CSS on unmount.

    Key behaviors:

    • Explicit props win: If you provide an explicit prop (e.g., innerBorder), it overrides the auto-extracted value for that specific key.
    • One-time extraction: CSS is read once on mount. For dynamic/animated values, you should use explicit effect props.
    • Limitations: Only the top border is read for auto-extraction (per-side borders are lost); border-image and outline are not supported; CSS transitions on borders/shadows won't work with auto-effects because the properties are stripped via inline styles.
    {/* The CSS border is automatically converted to an SVG inner border */}
    <SmoothCorners corners={{ radius: 24 }} style={{ border: "2px solid red", padding: 24 }}>
      Content with auto border
    </SmoothCorners>
  12. Configure Border and Shadow Effects

    main

    The EffectsConfig object allows you to define complex visual styles that are rendered via SVG overlays.

    Border Configuration (BorderConfig)

    • width: Border thickness.
    • color: A hex string or a GradientConfig.
    • opacity: Opacity level.
    • style: "solid" | "dashed" | "dotted" | "double" | "groove" | "ridge".
    • dash / gap: Used for dashed/dotted styles.
    • lineCap: "butt" | "round" | "square".

    Shadow Configuration (ShadowConfig)

    • offsetX, offsetY: Shadow displacement.
    • blur: Blur radius.
    • spread: Spread radius.
    • color: Hex color.
    • opacity: Opacity level.

    Effects Composition (EffectsConfig)

    Combine these into a single config:

    • innerBorder: Border inside the element.
    • outerBorder: Border outside the element.
    • middleBorder: Border between inner and outer.
    • innerShadow: Inset shadows (accepts ShadowConfig or ShadowConfig[]).
    • shadow: Outer shadows (accepts ShadowConfig or ShadowConfig[]).