diff --git a/.changeset/selector-affix-enhancement.md b/.changeset/selector-affix-enhancement.md
new file mode 100644
index 000000000..0d2639075
--- /dev/null
+++ b/.changeset/selector-affix-enhancement.md
@@ -0,0 +1,6 @@
+---
+"@cube-dev/ui-kit": patch
+---
+
+Enhanced selector affix syntax (`$`) for sub-element styling in tasty. Capitalized words in the affix are now automatically transformed to sub-element selectors, allowing complex selector chains like `$: '> Body > Row >'` which generates `.table > [data-element="Body"] > [data-element="Row"] > [data-element="Cell"]`.
+
diff --git a/src/stories/Tasty.docs.mdx b/src/stories/Tasty.docs.mdx
index b0dcf10b5..e59365b21 100644
--- a/src/stories/Tasty.docs.mdx
+++ b/src/stories/Tasty.docs.mdx
@@ -567,6 +567,33 @@ const Card = tasty({
```
+#### Selector Affix (`$`)
+
+Control sub-element selector combinator using the `$` property:
+
+```jsx
+const Table = tasty({
+ styles: {
+ // Direct child selector
+ Row: {
+ $: '>', // .table > [data-element="Row"]
+ padding: '1x',
+ },
+
+ // Chained selectors (auto-transforms capitalized words)
+ Cell: {
+ $: '> Body > Row >', // .table > [data-element="Body"] > [data-element="Row"] > [data-element="Cell"]
+ border: '1bw solid #border',
+ },
+
+ // Default: descendant selector (space)
+ Text: {
+ color: '#text', // .table [data-element="Text"]
+ },
+ },
+});
+```
+
### Variants & Theming
Create themed component variations:
diff --git a/src/tasty/tasty.test.tsx b/src/tasty/tasty.test.tsx
index 209f05eac..6989a0a58 100644
--- a/src/tasty/tasty.test.tsx
+++ b/src/tasty/tasty.test.tsx
@@ -1170,6 +1170,66 @@ describe('tastyGlobal() API', () => {
expect(styleContent).not.toMatch(/>\s*\[data-element="Content"\]/);
});
+ it('should support empty affix for sub-elements with $: ""', () => {
+ const Card = tasty({
+ styles: {
+ padding: '2x',
+ Content: {
+ $: '',
+ color: '#primary',
+ },
+ },
+ });
+
+ render(
+
+ Content text
+ ,
+ );
+
+ const styleElements = document.head.querySelectorAll('[data-tasty]');
+ const styleContent = Array.from(styleElements)
+ .map((el) => el.textContent)
+ .join('');
+
+ // Empty affix should result in descendant selector (space only)
+ expect(styleContent).toMatch(/\s+\[data-element="Content"\]/);
+ expect(styleContent).not.toMatch(/>\s*\[data-element="Content"\]/);
+ });
+
+ it('should support complex selector chain with $: "> Body > Row >"', () => {
+ const Table = tasty({
+ styles: {
+ display: 'table',
+ Cell: {
+ $: '> Body > Row >',
+ padding: '1x',
+ border: '1px solid #border',
+ },
+ },
+ });
+
+ render(
+
,
+ );
+
+ const styleElements = document.head.querySelectorAll('[data-tasty]');
+ const styleContent = Array.from(styleElements)
+ .map((el) => el.textContent)
+ .join('');
+
+ // Should transform to: > [data-element="Body"] > [data-element="Row"] > [data-element="Cell"]
+ expect(styleContent).toMatch(
+ />\s*\[data-element="Body"\]\s*>\s*\[data-element="Row"\]\s*>\s*\[data-element="Cell"\]/,
+ );
+ });
+
it('should support multiple global style components with different selectors', () => {
const GlobalHeading = tasty('h1.special', {
preset: 'h1',
diff --git a/src/tasty/utils/renderStyles.ts b/src/tasty/utils/renderStyles.ts
index dd2b965b1..956e04bb6 100644
--- a/src/tasty/utils/renderStyles.ts
+++ b/src/tasty/utils/renderStyles.ts
@@ -72,6 +72,27 @@ export function isSelector(key: string): boolean {
);
}
+/**
+ * Transform selector affix by converting capitalized words to sub-element selectors.
+ * Returns a selector prefix with leading and trailing spaces.
+ *
+ * Examples:
+ * '> Body > Row' -> ' > [data-element="Body"] > [data-element="Row"] '
+ * '> Body > Row >' -> ' > [data-element="Body"] > [data-element="Row"] > '
+ * '>' -> ' > '
+ */
+function transformSelectorAffix(affix: string): string {
+ const trimmed = affix.trim();
+ if (!trimmed) return ' ';
+
+ const tokens = trimmed.split(/\s+/);
+ const transformed = tokens.map((token) =>
+ /^[A-Z]/.test(token) ? `[data-element="${token}"]` : token,
+ );
+
+ return ` ${transformed.join(' ')} `;
+}
+
/**
* Get the selector suffix for a key
*/
@@ -85,9 +106,12 @@ function getSelector(key: string, styles?: Styles): string | null {
}
if (key.match(/^[A-Z]/)) {
- // Check if styles object has $: '>' for direct child selector
- const combinator = styles && (styles as any).$ === '>' ? ' > ' : ' ';
- return `${combinator}[data-element="${key}"]`;
+ const affix = styles?.$;
+ if (affix !== undefined) {
+ const prefix = transformSelectorAffix(String(affix));
+ return `${prefix}[data-element="${key}"]`;
+ }
+ return ` [data-element="${key}"]`;
}
return null;