Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/finsweet/attributes/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Attributes by Finsweet is a modular JavaScript library that dynamically loads and executes individual attribute solutions on demand. The core library handles initialization, dependency management, and provides a unified API through the global window.FinsweetAttributes object.

Architecture

The system is built on three core principles:
  1. Dynamic Loading - Attribute packages are only loaded when needed
  2. Monorepo Structure - Each attribute is a standalone package
  3. Global API - Unified interface for all attributes through window.FinsweetAttributes

Initialization Flow

1. Script Execution

When the core library script loads, it immediately runs the init() function:
// From packages/attributes/src/attributes.ts:19
const init = () => {
  const { FinsweetAttributes } = window;

  // Avoid initting the Attributes API more than once
  if (FinsweetAttributes && !Array.isArray(FinsweetAttributes)) {
    initAttributes();
    return;
  }

  // Collect pre-existing callbacks
  const callbacks = Array.isArray(FinsweetAttributes) 
    ? (FinsweetAttributes as FinsweetAttributesCallback[]) 
    : [];

  // Init Attributes object
  window.FinsweetAttributes = {
    version,
    scripts: [],
    modules: {},
    process: new Set<FinsweetAttributeKey>(),
    utils: { fetchPage, attachExternalStylesheets },
    load: initAttribute,
    push(...args) { /* ... */ },
    destroy() { /* ... */ }
  };
};
The initialization is designed to run safely multiple times. If window.FinsweetAttributes already exists as an object (not an array), it skips re-initialization and only initializes the individual attributes.

2. Detection Methods

The library supports two detection methods:

Method A: Script Tag Declaration

Declare which attributes to load directly on the <script> tag:
<script 
  type="module" 
  src="https://cdn.jsdelivr.net/npm/@finsweet/attributes@latest/attributes.js"
  fs-accordion
  fs-modal
></script>
The library scans for script tags with fs-{attribute} attributes:
// From packages/attributes/src/attributes.ts:82-87
for (const key of ATTRIBUTE_KEYS) {
  const isDefined = script.hasAttribute(`fs-${key}`);
  if (!isDefined) continue;

  initAttribute(key);
}

Method B: Auto-Detection

Enable automatic detection by adding fs-attributes-auto="true":
<script 
  type="module" 
  src="https://cdn.jsdelivr.net/npm/@finsweet/attributes@latest/attributes.js"
  fs-attributes-auto="true"
></script>
With auto-detection enabled, the library scans the entire DOM:
// From packages/attributes/src/attributes.ts:98-114
const usedAttributes = new Set<FinsweetAttributeKey>();
const allElements = document.querySelectorAll('*');

for (const element of allElements) {
  for (const name of element.getAttributeNames()) {
    const fsMatch = name.match(/^fs-([^-]+)/);
    const key = fsMatch?.[1] as FinsweetAttributeKey | undefined;

    if (key && ATTRIBUTE_KEYS.has(key)) {
      usedAttributes.add(key);
    }
  }
}

for (const attribute of usedAttributes) {
  initAttribute(attribute);
}
Auto-detection is convenient but scans every element in the DOM. For performance-critical applications with large DOMs, explicit script tag declaration is recommended.

3. Package Loading

Each attribute is loaded dynamically using dynamic imports:
// From packages/attributes/src/load.ts:7-11
export const loadAttribute = async (key: FinsweetAttributeKey) => {
  switch (key) {
    case 'accordion': {
      return import('@finsweet/attributes-accordion');
    }
    // ... other attributes
  }
};
This approach enables:
  • Code splitting - Only load what’s needed
  • Lazy loading - Defer loading until runtime
  • Bundle optimization - Reduce initial payload size

4. Attribute Initialization

Once loaded, each attribute package is initialized:
// From packages/attributes/src/attributes.ts:124-166
const initAttribute = async (key: FinsweetAttributeKey) => {
  // Ensure the attribute is only initted once
  if (window.FinsweetAttributes.process.has(key)) return;

  window.FinsweetAttributes.process.add(key);

  // Init controls
  const controls = (window.FinsweetAttributes.modules[key] ||= {});

  controls.loading = new Promise((resolve) => {
    controls.resolve = (value) => {
      resolve(value);
      delete controls.resolve;
    };
  });

  // Load Attribute package
  try {
    const { init, version } = await loadAttribute(key);

    // Init attribute
    const { result, destroy } = (await init()) || {};

    // Finalize controls
    controls.version = version;
    controls.destroy = () => {
      destroy?.();
      window.FinsweetAttributes.process.delete(key);
    };
    controls.restart = () => {
      controls.destroy?.();
      return window.FinsweetAttributes.load(key);
    };

    controls.resolve?.(result);
    return result;
  } catch (err) {
    console.error(err);
  }
};

Runtime Behavior

Deduplication

The library prevents duplicate initialization:
  • Script-level: Tracks processed script tags in window.FinsweetAttributes.scripts
  • Attribute-level: Uses a Set in window.FinsweetAttributes.process to track active attributes
// From packages/attributes/src/attributes.ts:76-77
if (window.FinsweetAttributes.scripts.includes(script)) return;
window.FinsweetAttributes.scripts.push(script);

Lifecycle Management

Each initialized attribute gets lifecycle controls:
const controls = window.FinsweetAttributes.modules.accordion;

// Wait for initialization
await controls.loading;

// Restart the attribute
await controls.restart?.();

// Destroy the attribute
controls.destroy?.();

Callback System

Run code after an attribute loads using the push method:
window.FinsweetAttributes = window.FinsweetAttributes || [];
window.FinsweetAttributes.push(
  ['accordion', (result) => {
    console.log('Accordion loaded:', result);
  }],
  ['modal', (result) => {
    console.log('Modal loaded:', result);
  }]
);
Callbacks can be pushed before the library loads. The core library collects any pre-existing callbacks from the array and executes them after initialization.

DOM Ready Handling

The library uses a two-phase initialization to handle different script loading scenarios:
// From packages/attributes/src/attributes.ts:90-94
getScripts().forEach(initScript);

await waitDOMReady();

getScripts().forEach(initScript);
  1. Immediate: Process scripts that exist when the core library executes
  2. After DOM Ready: Process scripts added dynamically or after DOM ready
This ensures attributes work regardless of when the script tag is loaded.

Error Handling

The library includes error handling at the attribute level:
try {
  const { init, version } = await loadAttribute(key);
  const { result, destroy } = (await init()) || {};
  // ... initialization
} catch (err) {
  console.error(err);
}
Errors during attribute loading or initialization are:
  • Logged to the console
  • Isolated to the failing attribute (other attributes continue to work)
  • Do not crash the entire library

Performance Considerations

Code Splitting Benefits

  • Smaller initial bundle: Core library is ~2KB gzipped
  • On-demand loading: Attributes load only when used
  • Parallel loading: Multiple attributes can load simultaneously

Optimization Recommendations

  1. Use explicit declaration instead of auto-detection for large DOMs
  2. Load only needed attributes via script tag attributes
  3. Leverage browser caching with CDN delivery
  4. Defer non-critical attributes using callbacks
The modular architecture means you only pay the performance cost for attributes you actually use. An empty page with just the core library adds minimal overhead.

Build docs developers (and LLMs) love