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.

16 stars

Best use case

fullstory-stable-selectors is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

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.

Teams using fullstory-stable-selectors should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/fullstory-stable-selectors/SKILL.md --create-dirs "https://raw.githubusercontent.com/diegosouzapw/awesome-omni-skill/main/skills/development/fullstory-stable-selectors/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/fullstory-stable-selectors/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How fullstory-stable-selectors Compares

Feature / Agentfullstory-stable-selectorsStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

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.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# 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

```html
<!-- 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:**

| Tool | Problem |
|------|---------|
| **Fullstory** | Searches break, defined elements stop matching, click maps lose continuity |
| **E2E Testing** | Cypress/Playwright tests become brittle |
| **AI Agents (CUA)** | Cannot reliably identify interactive elements |
| **Automation** | Scripts 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:

```html
<!-- 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)

| Attribute | Purpose | Case | Example |
|-----------|---------|------|---------|
| `data-component` | Component boundary identifier | PascalCase | `ProductCard`, `CheckoutForm` |
| `data-element` | Element role within component | kebab-case | `add-to-cart`, `price-display` |

#### Extended Attributes (Recommended for CUA/AI)

| Attribute | Purpose | When to Use |
|-----------|---------|-------------|
| `data-action` | Describes what happens on interaction | Buttons, links, toggles |
| `data-state` | Current state of the element | Expandable, toggleable elements |
| `data-variant` | Visual or functional variant | A/B tests, feature flags |
| `data-testid` | Unified test/automation identifier | When aligning with E2E tests |

#### Development Attributes (Strip in Production)

| Attribute | Purpose |
|-----------|---------|
| `data-source-file` | Source file reference for debugging |
| `data-source-line` | Line 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:

```html
<!-- 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:

```html
<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 Type | Purpose | Audience |
|----------------|---------|----------|
| `data-*` selectors | Stable programmatic targeting | Fullstory, Tests, AI Agents |
| `aria-*` attributes | Semantic meaning & relationships | Screen readers, AI understanding |
| `role` attribute | Element type override | Accessibility, 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:

```html
<!-- ✅ 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:

```html
<!-- ✅ 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:

```html
<!-- ✅ 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

```jsx
// 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)

```jsx
// 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

```html
<!-- 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)

```typescript
// 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

```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)

```javascript
// 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

```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:

```tsx
// 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)

```astro
---
// 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

```tsx
// 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:

```typescript
// 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

```javascript
// 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.)

```html
<!-- 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 Name | Selector |
|--------------|----------|
| 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:

```html
<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>
```

| Attribute | Purpose |
|-----------|---------|
| `data-component` | Stable selector for searching |
| `data-element` | Stable selector for specific element |
| `data-fs-element` | Fullstory defined element name |
| `data-fs-properties-schema` | Fullstory element properties schema |

---

## ✅ GOOD Implementation Examples

### Example 1: E-commerce Product Grid

```html
<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

```html
<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

```html
<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

```html
<!-- ❌ 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:**
```html
<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

```html
<!-- ❌ 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:**
```html
<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

```html
<!-- ❌ 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:**
```html
<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:

```tsx
// 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:

```javascript
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:

```html
<!-- 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:

```html
<!-- 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:

```tsx
// 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:

```html
<!-- 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:

```jsx
// ✅ 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:

```markdown
## 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

```html
<!-- 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:

```html
<!-- ✅ 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:

```javascript
// 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

```markdown
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
- **CSS Selectors in Search**: https://help.fullstory.com/hc/en-us/articles/360020623294
- **Defined Elements**: https://help.fullstory.com/hc/en-us/articles/360020828113
- **Element Properties Guide**: ../core/fullstory-element-properties/SKILL.md

### Testing Tool Integration
- **Cypress Best Practices (Selecting Elements)**: https://docs.cypress.io/guides/references/best-practices#Selecting-Elements
- **Playwright Locators**: https://playwright.dev/docs/locators
- **Testing Library Queries**: https://testing-library.com/docs/queries/about

### Accessibility & AI
- **WAI-ARIA Authoring Practices**: https://www.w3.org/WAI/ARIA/apg/
- **MDN: Using Data Attributes**: https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes

### 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.*

Related Skills

fullstory-component-wellbeing

16
from diegosouzapw/awesome-omni-skill

Expert guidance for monitoring frontend component health, performance, and rendering stability within Fullstory. Framework-agnostic patterns for React, Vue, Angular, Svelte, and React Native.

asyncredux-selectors

16
from diegosouzapw/awesome-omni-skill

Create and cache selectors for efficient state access. Covers writing selector functions, caching with `cache1` and `cache2`, the reselect pattern, and avoiding repeated computations in widgets.

bgo

10
from diegosouzapw/awesome-omni-skill

Automates the complete Blender build-go workflow, from building and packaging your extension/add-on to removing old versions, installing, enabling, and launching Blender for quick testing and iteration.

Coding & Development

iikit-07-analyze

16
from diegosouzapw/awesome-omni-skill

Validate cross-artifact consistency between spec, plan, and tasks

IFCore

16
from diegosouzapw/awesome-omni-skill

Use when developing on the IFCore compliance checker. Covers contracts, check function conventions, issue reporting, app structure, and development patterns.

idiomatic-go

16
from diegosouzapw/awesome-omni-skill

Write production-ready Go backends, CLIs, and APIs following modern best practices from top tier tech companies. Use this skill when creating or reviewing Go code for (1) backend services and APIs, (2) command-line tools, (3) code requiring proper error handling, concurrency, or testing patterns, (4) any Go development requiring adherence to established style guidelines. Includes comprehensive linting configuration and detailed style guide.

idempotency

16
from diegosouzapw/awesome-omni-skill

Idempotency concepts, patterns for APIs REST, database operations, and implementation strategies. Use when implementing idempotent operations, handling retries, or when ensuring operations can be safely repeated in any language.

icons

16
from diegosouzapw/awesome-omni-skill

SeekSaaS icon system guide covering @workspace/custom-icons package with Icons and AiIcons components

i18n-automation

16
from diegosouzapw/awesome-omni-skill

Automate internationalization and localization workflows for web applications with translation, key generation, and library setup

hytaleservers-workflow

16
from diegosouzapw/awesome-omni-skill

Standard workflow for HyTaleServers.tech development

hypercognitive-skill-compiler

16
from diegosouzapw/awesome-omni-skill

Transform complex skill requirements into complete Agent Skills using exhaustive hypercognitive compiler thinking patterns

hubspot-integration

16
from diegosouzapw/awesome-omni-skill

Expert patterns for HubSpot CRM integration including OAuth authentication, CRM objects, associations, batch operations, webhooks, and custom objects. Covers Node.js and Python SDKs. Use when: hubs...