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.
$ 安裝
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:
- Fullstory: Reliable search, defined elements, click maps
- Automated Testing: Stable E2E test selectors
- Computer User Agents (CUA): AI agents navigating your interface
- 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:
| 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:
<!-- 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:
<!-- 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 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:
<!-- ✅ 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 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:
<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
<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:
- Inspect the element
- Verify
data-componentanddata-elementattributes exist - 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
- Framework-agnostic solution: Works in React, Angular, Vue, Svelte, Next.js, Astro, vanilla JS, server-side templates
- Primary attributes:
data-component(PascalCase) anddata-element(kebab-case) - Extended attributes for AI/CUA:
data-action,data-state,data-variant - Name by purpose, not appearance: "add-to-cart" not "blue-button"
- Annotate interactive elements: Buttons, inputs, links, cards in lists
- Combine with Element Properties: Stable selectors for search, Element Properties for analytics data
- Combine with ARIA: Use both data-* and aria-* for maximum AI/accessibility compatibility
- 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-labelprovides human-readable context alongside data-* targeting - Consistent naming: AI agents learn patterns—be consistent across your codebase
Questions to Ask Developers
- "What framework are you using?" (React, Vue, Angular, Next.js, Astro, etc.)
- "Are your class names dynamic?" (CSS Modules, styled-components, Tailwind)
- "What elements do you need to reliably search for in Fullstory?"
- "Do you have a component naming convention already?"
- "Are you using E2E testing tools?" (May want to align with data-testid)
- "Do you use micro-frontends or multiple teams?" (Need namespace strategy)
- "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:
- Add new selector alongside old (don't remove immediately)
- Update Fullstory defined elements to use new selector
- Verify data continuity in Fullstory dashboards
- 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.
Repository
