--- 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 ```html ↑ 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): │ │ │ │ │ │ ✅ SEMANTIC (AI understands): │ │ │ │ │ │ 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 ``` **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 // 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 ``` | 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.] │ │ │ │ Examples: │ │ • ProductCard (simple) │ │ • CheckoutPaymentForm (domain + type) │ │ • Checkout.PaymentForm (namespaced for micro-frontends) │ │ │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ data-element: -[-] │ │ │ │ Examples: │ │ • add-to-cart (action verb) │ │ • product-image (subject + type) │ │ • shipping-address-input (subject + descriptor + type) │ │ • nav-item-products (type + qualifier) │ │ │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ data-action: [-] │ │ │ │ Examples: │ │ • add-item │ │ • submit-form │ │ • toggle-menu │ │ • expand-details │ │ • navigate-next │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### Component Names (`data-component`) Use **PascalCase** matching your component/class names: ```html
``` ### Element Names (`data-element`) Use **kebab-case** describing the element's purpose: ```html ``` ### Action Names (`data-action`) Use **verb-first kebab-case** describing the outcome: ```html View All ``` ### 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 (
{product.name}

{product.name}

${product.price}
) } ``` #### 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 (
) } ``` --- ### Angular ```html

{{ product.name }}

{{ product.price | currency }}
``` #### 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
``` --- ### Vue ```vue ``` #### 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 ;
``` --- ### Svelte ```svelte
{product.name}

{product.name}

${product.price}
``` --- ### 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 (
) } // Client component with interactivity ;('use client') function AddToCartButton({productId}: {productId: string}) { const [loading, setLoading] = useState(false) return ( ) } ``` --- ### Astro (Islands Architecture) ```astro --- // ProductCard.astro const { product } = Astro.props; ---

{product.name}

``` --- ### Solid.js ```tsx // ProductCard.tsx function ProductCard(props: {product: Product}) { return (

{props.product.name}

) } ``` --- ### 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 '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 { 'data-component': C; 'data-element'?: ElementName; 'data-action'?: string; 'data-state'?: string; } // Factory function export function createSelectors( component: C ): { root: StableSelectors; element: (name: ElementName, action?: string) => Partial>; } { 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 (
{/* TypeScript will error if you use 'invalid-element' */}
); } ``` --- ### Vanilla JavaScript / Web Components ```javascript // product-card.js class ProductCard extends HTMLElement { connectedCallback() { const product = JSON.parse(this.getAttribute('product')) this.innerHTML = `
${product.name}

${product.name}

$${product.price}
` 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

{{ $product->name }}

{{ product.name }}

<%= product.name %>

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

Featured Products

Wireless Headphones

$149.99 $199.99
``` ### Example 2: Multi-Step Form ```html
``` ### Example 3: Navigation with Dropdowns ```html
Company
Cart (3)
``` --- ## ❌ BAD Implementation Examples ### Example 1: Generic Names ```html
``` **Why it's bad:** Every component has "image", "text", "button" - searches return everything. **✅ CORRECTED:** ```html
``` ### Example 2: Position-Based Names ```html
First product
Second product
Third product
``` **Why it's bad:** If sort order changes, "item-0" is now a different product. **✅ CORRECTED:** ```html
First product
Second product
Third product
``` ### Example 3: Appearance-Based Names ```html
Navigation
``` **Why it's bad:** If design changes (blue → green, sidebar moves right), names become wrong. **✅ CORRECTED:** ```html
Navigation
``` --- ## 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 (
{({index, style}) => (
)}
) } ``` **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 = `
` } } // 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
``` **Namespace Convention**: `{Team}.{Component}` prevents collisions. --- ### A/B Tests and Feature Flags Track variants for analysis: ```html ``` **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 ( Loading... } > ) } function ProductDetailsContent({productId}) { const product = use(fetchProduct(productId)) return (
{/* content */}
) } ``` **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 ``` **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 (
) } ``` ### 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
``` ### 4. Use Consistent Depth Don't over-nest annotations: ```html
``` --- ## 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._