fullstory-stable-selectors

Framework-agnostic guide for implementing stable, semantic selectors in any web application. Solves the dynamic class name problem caused by CSS-in-JS, CSS Modules, and build tools. Includes patterns for React, Angular, Vue, Svelte, Next.js, Astro, and more. Future-proofed for Computer User Agents (CUA) and AI-powered automation tools. Provides TypeScript patterns, naming taxonomies, and enterprise-scale conventions.

$ Installer

git clone https://github.com/fullstorydev/fs-skills /tmp/fs-skills && cp -r /tmp/fs-skills/framework/fullstory-stable-selectors ~/.claude/skills/fs-skills

// tip: Run this command in your terminal to install the skill


name: fullstory-stable-selectors version: v2 description: Framework-agnostic guide for implementing stable, semantic selectors in any web application. Solves the dynamic class name problem caused by CSS-in-JS, CSS Modules, and build tools. Includes patterns for React, Angular, Vue, Svelte, Next.js, Astro, and more. Future-proofed for Computer User Agents (CUA) and AI-powered automation tools. Provides TypeScript patterns, naming taxonomies, and enterprise-scale conventions. related_skills:

  • fullstory-element-properties
  • fullstory-privacy-controls
  • fullstory-getting-started
  • universal-data-scoping-and-decoration

Fullstory Stable Selectors

Overview

Modern web applications use build tools and CSS methodologies that generate dynamic, unpredictable class names. This creates challenges for:

  1. Fullstory: Reliable search, defined elements, click maps
  2. Automated Testing: Stable E2E test selectors
  3. Computer User Agents (CUA): AI agents navigating your interface
  4. Accessibility Tools: Programmatic element identification

The Solution: Add stable, semantic data-* attributes that describe what the element is, not how it's styled.

This skill teaches you how to implement stable selectors in any framework without requiring external pluginsโ€”and future-proofs your application for AI-powered tooling.


The Problem

<!-- What your code looks like -->
<button className={styles.primaryButton}>Add to Cart</button>

<!-- What renders in the browser -->
<button class="Button_primaryButton__x7Ks2">Add to Cart</button>
                                    โ†‘
                        This hash changes every build!

Dynamic class names come from:

  • โŒ CSS Modules (hash suffixes)
  • โŒ styled-components / Emotion (random class names)
  • โŒ Tailwind CSS (class purging changes the set)
  • โŒ Build optimizations (minification, renaming)
  • โŒ Component libraries (internal naming conventions)
  • โŒ Shadow DOM / Web Components (encapsulated styles)

Impact:

ToolProblem
FullstorySearches break, defined elements stop matching, click maps lose continuity
E2E TestingCypress/Playwright tests become brittle
AI Agents (CUA)Cannot reliably identify interactive elements
AutomationScripts break on every deployment

Why This Matters for AI Agents (CUA)

Computer User Agentsโ€”AI systems that interact with web interfacesโ€”rely on stable, semantic identifiers to understand and navigate your application.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  HOW CUAs "SEE" YOUR INTERFACE                                          โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                         โ”‚
โ”‚  โŒ BRITTLE (AI struggles):                                             โ”‚
โ”‚     <button class="sc-3d8f2a btn_primary__xK7n2">Buy Now</button>      โ”‚
โ”‚                                                                         โ”‚
โ”‚  โœ… SEMANTIC (AI understands):                                          โ”‚
โ”‚     <button                                                             โ”‚
โ”‚       data-component="ProductCard"                                      โ”‚
โ”‚       data-element="purchase-button"                                    โ”‚
โ”‚       data-action="add-to-cart"                                        โ”‚
โ”‚       aria-label="Add to cart"                                          โ”‚
โ”‚     >Buy Now</button>                                                   โ”‚
โ”‚                                                                         โ”‚
โ”‚  The AI can now reliably:                                               โ”‚
โ”‚  โ€ข Find "the purchase button in ProductCard"                           โ”‚
โ”‚  โ€ข Understand the action it will trigger                               โ”‚
โ”‚  โ€ข Maintain stable automation across deployments                        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Stable selectors provide CUAs with:

  • โœ… Consistent element identification across builds
  • โœ… Semantic understanding of element purpose
  • โœ… Hierarchical context (component โ†’ element relationship)
  • โœ… Action hints for interaction planning

The Solution

Add stable data-* attributes that survive build changes:

<!-- Before: Brittle selector -->
<button class="Button_primaryButton__x7Ks2">Add to Cart</button>

<!-- After: Stable selector -->
<button 
  class="Button_primaryButton__x7Ks2"
  data-component="ProductCard"
  data-element="add-to-cart-button"
>
  Add to Cart
</button>

Benefits:

  • โœ… Survives all build changes
  • โœ… Semantic and self-documenting
  • โœ… Works in ANY framework
  • โœ… Enables reliable Fullstory searches
  • โœ… Powers defined elements and click maps
  • โœ… No external plugins required

Core Concepts

The Attribute Taxonomy

Primary Attributes (Required)

AttributePurposeCaseExample
data-componentComponent boundary identifierPascalCaseProductCard, CheckoutForm
data-elementElement role within componentkebab-caseadd-to-cart, price-display

Extended Attributes (Recommended for CUA/AI)

AttributePurposeWhen to Use
data-actionDescribes what happens on interactionButtons, links, toggles
data-stateCurrent state of the elementExpandable, toggleable elements
data-variantVisual or functional variantA/B tests, feature flags
data-testidUnified test/automation identifierWhen aligning with E2E tests

Development Attributes (Strip in Production)

AttributePurpose
data-source-fileSource file reference for debugging
data-source-lineLine number for debugging

Attribute Hierarchy

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  SEMANTIC HIERARCHY                                                      โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                         โ”‚
โ”‚  data-component="CheckoutForm"                    โ† Component boundary  โ”‚
โ”‚  โ”‚                                                                      โ”‚
โ”‚  โ”œโ”€โ”€ data-element="shipping-section"              โ† Structural element  โ”‚
โ”‚  โ”‚   โ”œโ”€โ”€ data-element="address-input"             โ† Interactive element โ”‚
โ”‚  โ”‚   โ””โ”€โ”€ data-element="city-input"                                      โ”‚
โ”‚  โ”‚                                                                      โ”‚
โ”‚  โ”œโ”€โ”€ data-element="payment-section"                                     โ”‚
โ”‚  โ”‚   โ””โ”€โ”€ data-element="card-input" + data-action="capture-payment"     โ”‚
โ”‚  โ”‚                                                                      โ”‚
โ”‚  โ””โ”€โ”€ data-element="submit-button"                                       โ”‚
โ”‚      + data-action="complete-purchase"            โ† Action hint for AI  โ”‚
โ”‚      + data-state="enabled|disabled|loading"      โ† Current state       โ”‚
โ”‚                                                                         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Aligning with Testing Tools

Many teams already use data-testid for Cypress/Playwright. You can unify:

<!-- Option 1: Use both (redundant but safe) -->
<button 
  data-element="add-to-cart"
  data-testid="add-to-cart-button"
>Add</button>

<!-- Option 2: Configure test tools to use data-element -->
// cypress.config.js
Cypress.SelectorPlayground.defaults({
  selectorPriority: ['data-element', 'data-component', 'data-testid', 'id']
});

// playwright.config.js
use: {
  testIdAttribute: 'data-element'
}

Integration with ARIA (Accessibility + AI)

Stable selectors complement ARIA attributesโ€”use both:

<button
  data-component="ProductCard"
  data-element="add-to-cart"
  data-action="add-item"
  aria-label="Add Wireless Headphones to cart"
  aria-describedby="price-123"
>
  Add to Cart
</button>
Attribute TypePurposeAudience
data-* selectorsStable programmatic targetingFullstory, Tests, AI Agents
aria-* attributesSemantic meaning & relationshipsScreen readers, AI understanding
role attributeElement type overrideAccessibility, AI categorization

CUA Best Practice: AI agents use BOTH data-* attributes for reliable targeting AND aria-* attributes for understanding element purpose and relationships.


Naming Conventions

Formal Naming Grammar

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  NAMING GRAMMAR                                                          โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                         โ”‚
โ”‚  data-component: [Namespace.]<Domain><Type>                             โ”‚
โ”‚                                                                         โ”‚
โ”‚    Examples:                                                            โ”‚
โ”‚    โ€ข ProductCard         (simple)                                       โ”‚
โ”‚    โ€ข CheckoutPaymentForm (domain + type)                               โ”‚
โ”‚    โ€ข Checkout.PaymentForm (namespaced for micro-frontends)             โ”‚
โ”‚                                                                         โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                         โ”‚
โ”‚  data-element: <subject>-<descriptor>[-<qualifier>]                     โ”‚
โ”‚                                                                         โ”‚
โ”‚    Examples:                                                            โ”‚
โ”‚    โ€ข add-to-cart         (action verb)                                  โ”‚
โ”‚    โ€ข product-image       (subject + type)                              โ”‚
โ”‚    โ€ข shipping-address-input (subject + descriptor + type)              โ”‚
โ”‚    โ€ข nav-item-products   (type + qualifier)                            โ”‚
โ”‚                                                                         โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                         โ”‚
โ”‚  data-action: <verb>[-<object>]                                         โ”‚
โ”‚                                                                         โ”‚
โ”‚    Examples:                                                            โ”‚
โ”‚    โ€ข add-item                                                           โ”‚
โ”‚    โ€ข submit-form                                                        โ”‚
โ”‚    โ€ข toggle-menu                                                        โ”‚
โ”‚    โ€ข expand-details                                                     โ”‚
โ”‚    โ€ข navigate-next                                                      โ”‚
โ”‚                                                                         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Component Names (data-component)

Use PascalCase matching your component/class names:

<!-- โœ… GOOD: Matches component names -->
<div data-component="ProductCard">
<div data-component="CheckoutForm">
<div data-component="NavigationHeader">
<div data-component="UserProfileDropdown">

<!-- โœ… GOOD: Namespaced for micro-frontends -->
<div data-component="Checkout.PaymentForm">
<div data-component="Catalog.ProductCard">

<!-- โŒ BAD: Generic names -->
<div data-component="Container">
<div data-component="Wrapper">
<div data-component="Component">
<div data-component="Box">

Element Names (data-element)

Use kebab-case describing the element's purpose:

<!-- โœ… GOOD: Describes purpose -->
<button data-element="add-to-cart">
<input data-element="email-input">
<div data-element="product-image">
<span data-element="price-display">
<nav data-element="main-navigation">

<!-- โœ… GOOD: Qualified names for disambiguation -->
<input data-element="billing-address-line1">
<input data-element="shipping-address-line1">

<!-- โŒ BAD: Describes appearance or position -->
<button data-element="blue-button">
<button data-element="big-button">
<button data-element="button-1">
<button data-element="first-button">
<div data-element="left-sidebar">

Action Names (data-action)

Use verb-first kebab-case describing the outcome:

<!-- โœ… GOOD: Clear action verbs -->
<button data-action="add-item">Add to Cart</button>
<button data-action="submit-order">Complete Purchase</button>
<button data-action="toggle-filter">Show Filters</button>
<a data-action="navigate-category">View All</a>

<!-- โŒ BAD: Nouns or unclear -->
<button data-action="cart">Add to Cart</button>
<button data-action="click-handler">Submit</button>

What to Annotate

Always annotate:

  • โœ… Buttons and clickable elements
  • โœ… Form inputs (text, select, checkbox, etc.)
  • โœ… Links and navigation items
  • โœ… Cards and list items in repeating content
  • โœ… Modals and dialog triggers
  • โœ… Tab and accordion controls

Skip annotation for:

  • โŒ Pure layout wrappers (unless interactive)
  • โŒ Styling containers
  • โŒ Text-only elements (unless key content)

Implementation by Framework

React

// ProductCard.jsx
function ProductCard({ product, onAddToCart }) {
  return (
    <div 
      data-component="ProductCard"
      data-element="card"
      className={styles.card}
    >
      <img 
        src={product.image} 
        alt={product.name}
        data-element="product-image"
      />
      
      <h3 data-element="product-name">{product.name}</h3>
      
      <span data-element="price">${product.price}</span>
      
      <button 
        data-element="add-to-cart"
        onClick={() => onAddToCart(product)}
      >
        Add to Cart
      </button>
    </div>
  );
}

React Helper (Optional)

// useStableSelector.js
export function useStableSelector(componentName) {
  return {
    root: {
      'data-component': componentName,
    },
    element: (name) => ({
      'data-element': name,
    }),
  };
}

// Usage
function ProductCard({ product }) {
  const sel = useStableSelector('ProductCard');
  
  return (
    <div {...sel.root} {...sel.element('card')}>
      <button {...sel.element('add-to-cart')}>Add to Cart</button>
    </div>
  );
}

Angular

<!-- product-card.component.html -->
<article 
  data-component="ProductCard"
  data-element="card"
  class="product-card"
>
  <img 
    [src]="product.image" 
    [alt]="product.name"
    data-element="product-image"
  />
  
  <h3 data-element="product-name">{{ product.name }}</h3>
  
  <span data-element="price">{{ product.price | currency }}</span>
  
  <button 
    data-element="add-to-cart"
    (click)="addToCart()"
  >
    Add to Cart
  </button>
</article>

Angular Directive (Optional)

// stable-selector.directive.ts
import { Directive, ElementRef, Input, OnInit } from '@angular/core';

@Directive({
  selector: '[fsComponent], [fsElement]'
})
export class StableSelectorDirective implements OnInit {
  @Input() fsComponent: string;
  @Input() fsElement: string;
  
  constructor(private el: ElementRef) {}
  
  ngOnInit() {
    if (this.fsComponent) {
      this.el.nativeElement.setAttribute('data-component', this.fsComponent);
    }
    if (this.fsElement) {
      this.el.nativeElement.setAttribute('data-element', this.fsElement);
    }
  }
}

// Usage in template
<div fsComponent="ProductCard" fsElement="card">
  <button fsElement="add-to-cart">Add to Cart</button>
</div>

Vue

<!-- ProductCard.vue -->
<template>
  <article 
    data-component="ProductCard"
    data-element="card"
    class="product-card"
  >
    <img 
      :src="product.image" 
      :alt="product.name"
      data-element="product-image"
    />
    
    <h3 data-element="product-name">{{ product.name }}</h3>
    
    <span data-element="price">{{ formatPrice(product.price) }}</span>
    
    <button 
      data-element="add-to-cart"
      @click="$emit('add-to-cart', product)"
    >
      Add to Cart
    </button>
  </article>
</template>

<script setup>
defineProps(['product']);
defineEmits(['add-to-cart']);
</script>

Vue Directive (Optional)

// main.js
app.directive('fs', {
  mounted(el, binding) {
    const { component, element } = binding.value;
    if (component) el.setAttribute('data-component', component);
    if (element) el.setAttribute('data-element', element);
  }
});

// Usage in template
<div v-fs="{ component: 'ProductCard', element: 'card' }">
  <button v-fs="{ element: 'add-to-cart' }">Add to Cart</button>
</div>

Svelte

<!-- ProductCard.svelte -->
<article 
  data-component="ProductCard"
  data-element="card"
  class="product-card"
>
  <img 
    src={product.image} 
    alt={product.name}
    data-element="product-image"
  />
  
  <h3 data-element="product-name">{product.name}</h3>
  
  <span data-element="price">${product.price}</span>
  
  <button 
    data-element="add-to-cart"
    data-action="add-item"
    on:click={() => dispatch('addToCart', product)}
  >
    Add to Cart
  </button>
</article>

<script>
  import { createEventDispatcher } from 'svelte';
  export let product;
  const dispatch = createEventDispatcher();
</script>

Next.js (App Router / React Server Components)

Server components work identicallyโ€”data attributes render to HTML:

// app/products/[id]/page.tsx (Server Component)
export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id);
  
  return (
    <main data-component="ProductPage" data-element="page">
      <ProductDetails product={product} />
      <AddToCartButton productId={product.id} />
    </main>
  );
}

// Client component with interactivity
'use client';
function AddToCartButton({ productId }: { productId: string }) {
  const [loading, setLoading] = useState(false);
  
  return (
    <button
      data-component="AddToCartButton"
      data-element="trigger"
      data-action="add-to-cart"
      data-state={loading ? 'loading' : 'idle'}
      data-product-id={productId}
      onClick={handleClick}
    >
      {loading ? 'Adding...' : 'Add to Cart'}
    </button>
  );
}

Astro (Islands Architecture)

---
// ProductCard.astro
const { product } = Astro.props;
---

<article 
  data-component="ProductCard"
  data-element="card"
  data-product-id={product.id}
>
  <img src={product.image} data-element="product-image" />
  <h3 data-element="product-name">{product.name}</h3>
  
  <!-- Interactive island -->
  <AddToCartButton client:visible productId={product.id} />
</article>

Solid.js

// ProductCard.tsx
function ProductCard(props: { product: Product }) {
  return (
    <article data-component="ProductCard" data-element="card">
      <img src={props.product.image} data-element="product-image" />
      <h3 data-element="product-name">{props.product.name}</h3>
      <button
        data-element="add-to-cart"
        data-action="add-item"
        onClick={() => addToCart(props.product)}
      >
        Add to Cart
      </button>
    </article>
  );
}

TypeScript Type-Safe Selectors

Create compile-time safety for your selector values:

// selectors.ts

// Define your component names as a union type
type ComponentName = 
  | 'ProductCard'
  | 'CheckoutForm'
  | 'NavigationHeader'
  | 'UserProfile'
  | 'CartDrawer';

// Define element names per component
type ElementName<C extends ComponentName> = 
  C extends 'ProductCard' ? 'card' | 'product-image' | 'product-name' | 'price' | 'add-to-cart' :
  C extends 'CheckoutForm' ? 'form' | 'shipping-section' | 'payment-section' | 'submit-button' :
  C extends 'CartDrawer' ? 'drawer' | 'item-list' | 'total' | 'checkout-button' :
  string;

// Type-safe selector builder
interface StableSelectors<C extends ComponentName> {
  'data-component': C;
  'data-element'?: ElementName<C>;
  'data-action'?: string;
  'data-state'?: string;
}

// Factory function
export function createSelectors<C extends ComponentName>(
  component: C
): {
  root: StableSelectors<C>;
  element: (name: ElementName<C>, action?: string) => Partial<StableSelectors<C>>;
} {
  return {
    root: { 'data-component': component },
    element: (name, action) => ({
      'data-element': name,
      ...(action && { 'data-action': action })
    })
  };
}

// Usage
function ProductCard({ product }: Props) {
  const sel = createSelectors('ProductCard');
  
  return (
    <div {...sel.root} {...sel.element('card')}>
      {/* TypeScript will error if you use 'invalid-element' */}
      <button {...sel.element('add-to-cart', 'add-item')}>
        Add to Cart
      </button>
    </div>
  );
}

Vanilla JavaScript / Web Components

// product-card.js
class ProductCard extends HTMLElement {
  connectedCallback() {
    const product = JSON.parse(this.getAttribute('product'));
    
    this.innerHTML = `
      <article data-component="ProductCard" data-element="card">
        <img 
          src="${product.image}" 
          alt="${product.name}"
          data-element="product-image"
        />
        <h3 data-element="product-name">${product.name}</h3>
        <span data-element="price">$${product.price}</span>
        <button data-element="add-to-cart">Add to Cart</button>
      </article>
    `;
    
    this.querySelector('[data-element="add-to-cart"]')
      .addEventListener('click', () => this.handleAddToCart(product));
  }
}

customElements.define('product-card', ProductCard);

Server-Side Templates (PHP, Django, Rails, etc.)

<!-- PHP/Blade -->
<article data-component="ProductCard" data-element="card">
  <img src="{{ $product->image }}" data-element="product-image" />
  <h3 data-element="product-name">{{ $product->name }}</h3>
  <button data-element="add-to-cart">Add to Cart</button>
</article>

<!-- Django -->
<article data-component="ProductCard" data-element="card">
  <img src="{{ product.image }}" data-element="product-image" />
  <h3 data-element="product-name">{{ product.name }}</h3>
  <button data-element="add-to-cart">Add to Cart</button>
</article>

<!-- Rails ERB -->
<article data-component="ProductCard" data-element="card">
  <img src="<%= product.image %>" data-element="product-image" />
  <h3 data-element="product-name"><%= product.name %></h3>
  <button data-element="add-to-cart">Add to Cart</button>
</article>

Using Stable Selectors in Fullstory

Searching by Selector

# Find all ProductCard components
css selector: [data-component="ProductCard"]

# Find add-to-cart buttons
css selector: [data-element="add-to-cart"]

# Find add-to-cart within ProductCard
css selector: [data-component="ProductCard"] [data-element="add-to-cart"]

Creating Defined Elements

When creating defined elements in Fullstory, use stable selectors:

Element NameSelector
Add to Cart Button[data-element="add-to-cart"]
Product Card[data-component="ProductCard"]
Search Input[data-element="search-input"]
Checkout Submit[data-component="CheckoutForm"] [data-element="submit-button"]

Combining with Element Properties

Stable selectors and Element Properties work together:

<div 
  data-component="ProductCard"
  data-element="card"
  data-fs-element="Product Card"
  data-fs-properties-schema='{"product_id":"string","price":"real"}'
  data-product-id="SKU-123"
  data-price="99.99"
>
  <!-- content -->
</div>
AttributePurpose
data-componentStable selector for searching
data-elementStable selector for specific element
data-fs-elementFullstory defined element name
data-fs-properties-schemaFullstory element properties schema

โœ… GOOD Implementation Examples

Example 1: E-commerce Product Grid

<section data-component="ProductGrid" data-element="grid">
  <h2 data-element="section-title">Featured Products</h2>
  
  <div data-element="product-list">
    <!-- Each product card -->
    <article data-component="ProductCard" data-element="card">
      <img src="..." data-element="product-image" />
      <h3 data-element="product-name">Wireless Headphones</h3>
      <div data-element="pricing">
        <span data-element="current-price">$149.99</span>
        <span data-element="original-price">$199.99</span>
      </div>
      <div data-element="actions">
        <button data-element="add-to-cart">Add to Cart</button>
        <button data-element="wishlist">โ™ก</button>
      </div>
    </article>
    
    <!-- More product cards... -->
  </div>
  
  <nav data-element="pagination">
    <button data-element="prev-page">Previous</button>
    <button data-element="next-page">Next</button>
  </nav>
</section>

Example 2: Multi-Step Form

<form data-component="CheckoutForm" data-element="form">
  <!-- Progress indicator -->
  <nav data-element="step-indicator">
    <span data-element="step" data-step="shipping">Shipping</span>
    <span data-element="step" data-step="payment">Payment</span>
    <span data-element="step" data-step="review">Review</span>
  </nav>
  
  <!-- Shipping step -->
  <fieldset data-element="shipping-step">
    <div data-element="name-field">
      <label>Full Name</label>
      <input type="text" data-element="name-input" />
    </div>
    <div data-element="address-field">
      <label>Address</label>
      <input type="text" data-element="address-input" />
    </div>
  </fieldset>
  
  <!-- Payment step (with privacy) -->
  <fieldset data-element="payment-step" class="fs-exclude">
    <div data-element="card-field">
      <label>Card Number</label>
      <input type="text" data-element="card-input" />
    </div>
  </fieldset>
  
  <!-- Actions -->
  <div data-element="form-actions">
    <button type="button" data-element="back-button">Back</button>
    <button type="submit" data-element="submit-button">Continue</button>
  </div>
</form>

Example 3: Navigation with Dropdowns

<header data-component="SiteHeader" data-element="header">
  <a href="/" data-element="logo">
    <img src="logo.svg" alt="Company" />
  </a>
  
  <nav data-component="MainNav" data-element="navigation">
    <ul data-element="nav-list">
      <li data-element="nav-item">
        <a href="/products" data-element="nav-link">Products</a>
        <ul data-element="dropdown-menu">
          <li><a href="/products/shoes" data-element="dropdown-item">Shoes</a></li>
          <li><a href="/products/bags" data-element="dropdown-item">Bags</a></li>
        </ul>
      </li>
      <li data-element="nav-item">
        <a href="/about" data-element="nav-link">About</a>
      </li>
    </ul>
  </nav>
  
  <div data-element="header-actions">
    <button data-element="search-toggle">๐Ÿ”</button>
    <a href="/cart" data-element="cart-link">
      Cart (<span data-element="cart-count">3</span>)
    </a>
    <button data-element="account-menu">Account</button>
  </div>
</header>

โŒ BAD Implementation Examples

Example 1: Generic Names

<!-- โŒ BAD: Names are too generic -->
<div data-component="Component">
  <img data-element="image" />
  <span data-element="text" />
  <button data-element="button">Click</button>
</div>

Why it's bad: Every component has "image", "text", "button" - searches return everything.

โœ… CORRECTED:

<div data-component="ProductCard">
  <img data-element="product-image" />
  <span data-element="product-name" />
  <button data-element="add-to-cart">Click</button>
</div>

Example 2: Position-Based Names

<!-- โŒ BAD: Position-based naming -->
<div data-component="ProductList">
  <div data-element="item-0">First product</div>
  <div data-element="item-1">Second product</div>
  <div data-element="item-2">Third product</div>
</div>

Why it's bad: If sort order changes, "item-0" is now a different product.

โœ… CORRECTED:

<div data-component="ProductList">
  <div data-element="product-item" data-product-id="SKU-A">First product</div>
  <div data-element="product-item" data-product-id="SKU-B">Second product</div>
  <div data-element="product-item" data-product-id="SKU-C">Third product</div>
</div>

Example 3: Appearance-Based Names

<!-- โŒ BAD: Named by appearance -->
<button data-element="blue-button">Primary Action</button>
<button data-element="gray-button">Secondary Action</button>
<div data-element="left-sidebar">Navigation</div>

Why it's bad: If design changes (blue โ†’ green, sidebar moves right), names become wrong.

โœ… CORRECTED:

<button data-element="primary-action">Primary Action</button>
<button data-element="secondary-action">Secondary Action</button>
<div data-element="side-navigation">Navigation</div>

Advanced Patterns

Virtualized Lists / Infinite Scroll

For virtualized content where DOM elements are recycled:

// React with react-window or react-virtualized
function VirtualizedProductList({ products }) {
  return (
    <div data-component="ProductList" data-element="virtual-container">
      <FixedSizeList
        height={600}
        itemCount={products.length}
        itemSize={120}
      >
        {({ index, style }) => (
          <div
            style={style}
            data-element="product-row"
            data-row-index={index}
            data-product-id={products[index].id}  // Stable ID, not position!
          >
            <ProductCard product={products[index]} />
          </div>
        )}
      </FixedSizeList>
    </div>
  );
}

Key Principle: Use stable business identifiers (data-product-id), not positional indices.


Shadow DOM / Web Components

Shadow DOM encapsulates styles but data attributes still work:

class ProductCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }
  
  connectedCallback() {
    // Set attributes on the host element (light DOM)
    this.setAttribute('data-component', 'ProductCard');
    this.setAttribute('data-element', 'card');
    
    // Shadow DOM content also gets attributes
    this.shadowRoot.innerHTML = `
      <style>/* encapsulated styles */</style>
      <article>
        <slot name="image"></slot>
        <button data-element="add-to-cart" data-action="add-item">
          <slot name="button-text">Add to Cart</slot>
        </button>
      </article>
    `;
  }
}

// For Fullstory to see shadow DOM content, enable deep capture:
// FS.setProperties({ type: 'page', properties: { shadowDomEnabled: true } });

Fullstory Note: Contact Fullstory support about Shadow DOM capture configuration for your account.


Micro-Frontends

When multiple teams own different parts of the UI, namespace your selectors:

<!-- Team Checkout owns this -->
<div 
  data-component="Checkout.PaymentForm"
  data-team="checkout"
  data-element="form"
>
  <button data-element="submit-payment">Pay</button>
</div>

<!-- Team Catalog owns this -->
<div 
  data-component="Catalog.ProductCard"
  data-team="catalog"
  data-element="card"
>
  <button data-element="add-to-cart">Add</button>
</div>

Namespace Convention: {Team}.{Component} prevents collisions.


A/B Tests and Feature Flags

Track variants for analysis:

<!-- Variant A: Original -->
<button
  data-component="CTAButton"
  data-element="hero-cta"
  data-variant="control"
  data-experiment="homepage-cta-2024"
>
  Get Started
</button>

<!-- Variant B: Test -->
<button
  data-component="CTAButton"
  data-element="hero-cta"
  data-variant="treatment-green"
  data-experiment="homepage-cta-2024"
>
  Start Free Trial
</button>

In Fullstory: Search by [data-experiment="homepage-cta-2024"][data-variant="treatment-green"] to analyze specific variants.


Dynamic/Lazy-Loaded Content

Ensure selectors are present when content loads:

// React with Suspense
function ProductDetails({ productId }) {
  return (
    <Suspense 
      fallback={
        <div 
          data-component="ProductDetails" 
          data-element="skeleton" 
          data-state="loading"
        >
          Loading...
        </div>
      }
    >
      <ProductDetailsContent productId={productId} />
    </Suspense>
  );
}

function ProductDetailsContent({ productId }) {
  const product = use(fetchProduct(productId));
  
  return (
    <div 
      data-component="ProductDetails" 
      data-element="content"
      data-state="loaded"
      data-product-id={productId}
    >
      {/* content */}
    </div>
  );
}

Note: The data-state attribute helps distinguish loading vs loaded states in Fullstory searches.


Iframes (Cross-Origin Limitations)

For same-origin iframes, selectors work normally. For cross-origin:

<!-- Parent page -->
<iframe 
  src="https://checkout.example.com/embed"
  data-component="CheckoutEmbed"
  data-element="iframe"
  title="Checkout"
></iframe>

Limitation: Fullstory cannot directly capture cross-origin iframe content. The iframe must have its own Fullstory snippet installed.


Best Practices

1. Annotate at Development Time

Add annotations as you write components, not as an afterthought:

// โœ… Good habit: Add annotations as you code
function ProductCard({ product }) {
  return (
    <div data-component="ProductCard">
      <button data-element="add-to-cart">Add</button>
    </div>
  );
}

2. Document Your Conventions

Create a team style guide:

## Stable Selector Conventions

### Component Names
- Use PascalCase: `ProductCard`, `CheckoutForm`
- Match your component file/class name

### Element Names
- Use kebab-case: `add-to-cart`, `search-input`
- Describe purpose, not appearance
- Be specific: `product-name` not `name`

### Required Annotations
- All buttons and links
- All form inputs
- All cards in lists
- Modal and dropdown triggers

3. Combine with Privacy Controls

<!-- Annotate, but respect privacy -->
<form data-component="PaymentForm">
  <div data-element="card-field" class="fs-exclude">
    <input data-element="card-input" type="text" />
  </div>
  <button data-element="submit-payment">Pay Now</button>
</form>

4. Use Consistent Depth

Don't over-nest annotations:

<!-- โœ… GOOD: Flat, specific selectors -->
<div data-component="ProductCard">
  <button data-element="add-to-cart">Add</button>
</div>

<!-- โŒ BAD: Deep nesting (unnecessary) -->
<div data-component="App">
  <div data-component="MainContent">
    <div data-component="ProductSection">
      <div data-component="ProductCard">
        <button data-element="add-to-cart">Add</button>
      </div>
    </div>
  </div>
</div>

Troubleshooting

Selectors Not Working in Fullstory

Check in browser DevTools:

  1. Inspect the element
  2. Verify data-component and data-element attributes exist
  3. Check for typos in attribute names

Common issues:

  • Framework stripping data attributes in production
  • SSR/hydration mismatch
  • Conditional rendering removing the element

Too Many Search Results

Problem: Searching [data-element="button"] returns hundreds of results

Solution: Be more specific:

[data-component="ProductCard"] [data-element="add-to-cart"]

Attributes Stripped in Production

Check your build tool configuration:

// webpack.config.js - DON'T strip data-* attributes
optimization: {
  minimizer: [
    new HtmlWebpackPlugin({
      minify: {
        // Keep data-* attributes
        removeDataAttributes: false
      }
    })
  ]
}

KEY TAKEAWAYS FOR AGENT

When helping developers implement stable selectors:

Core Principles

  1. Framework-agnostic solution: Works in React, Angular, Vue, Svelte, Next.js, Astro, vanilla JS, server-side templates
  2. Primary attributes: data-component (PascalCase) and data-element (kebab-case)
  3. Extended attributes for AI/CUA: data-action, data-state, data-variant
  4. Name by purpose, not appearance: "add-to-cart" not "blue-button"
  5. Annotate interactive elements: Buttons, inputs, links, cards in lists
  6. Combine with Element Properties: Stable selectors for search, Element Properties for analytics data
  7. Combine with ARIA: Use both data-* and aria-* for maximum AI/accessibility compatibility
  8. No plugins required: Manual annotation works everywhere

CUA/AI Agent Considerations

  • data-action: Helps AI understand what interaction will do ("add-item", "submit-form", "toggle-menu")
  • data-state: Helps AI understand current element state ("loading", "disabled", "expanded")
  • ARIA integration: Ensure aria-label provides human-readable context alongside data-* targeting
  • Consistent naming: AI agents learn patternsโ€”be consistent across your codebase

Questions to Ask Developers

  1. "What framework are you using?" (React, Vue, Angular, Next.js, Astro, etc.)
  2. "Are your class names dynamic?" (CSS Modules, styled-components, Tailwind)
  3. "What elements do you need to reliably search for in Fullstory?"
  4. "Do you have a component naming convention already?"
  5. "Are you using E2E testing tools?" (May want to align with data-testid)
  6. "Do you use micro-frontends or multiple teams?" (Need namespace strategy)
  7. "Is AI/automation tooling on your roadmap?" (Add extended attributes now)

Implementation Checklist

Phase 1: Core Implementation
โ–ก Identify interactive elements that need tracking
โ–ก Add data-component to component root elements  
โ–ก Add data-element to buttons, inputs, links, cards
โ–ก Use specific, purpose-based names (not appearance/position)
โ–ก Test selectors in browser DevTools
โ–ก Verify attributes survive production build
โ–ก Create defined elements in Fullstory using data-* selectors

Phase 2: AI/CUA Readiness (Recommended)
โ–ก Add data-action to buttons and interactive elements
โ–ก Add data-state for elements with multiple states
โ–ก Ensure ARIA attributes complement data-* selectors
โ–ก Document naming conventions for team consistency

Phase 3: Enterprise Scale (If Applicable)
โ–ก Implement TypeScript type-safe selectors
โ–ก Add namespace prefixes for micro-frontends
โ–ก Add data-variant for A/B test tracking
โ–ก Configure E2E tools to use data-element

Selector Evolution Strategy

When you need to change selectors:

  1. Add new selector alongside old (don't remove immediately)
  2. Update Fullstory defined elements to use new selector
  3. Verify data continuity in Fullstory dashboards
  4. Remove old selector after confirming migration

REFERENCE LINKS

Fullstory Documentation

Testing Tool Integration

Accessibility & AI

Historical Context

These skills consolidate and extend patterns from:

  • fullstorydev/eslint-plugin-annotate-react (React-specific)
  • fullstorydev/fullstory-babel-plugin-annotate-react (Build-time injection)

The manual approach in this skill is more flexible and works across all frameworks.


This skill provides a universal, future-proof pattern for stable selectors that works in any framework. Optimized for Fullstory analytics, E2E testing, and AI-powered Computer User Agents. No external plugins required.