diff --git a/.agents/skills/add-new-component/SKILL.md b/.agents/skills/add-new-component/SKILL.md index 324587942..cfc7a4739 100644 --- a/.agents/skills/add-new-component/SKILL.md +++ b/.agents/skills/add-new-component/SKILL.md @@ -77,6 +77,7 @@ Key rules: - `displayName` set for React DevTools (e.g., `'Component.Trigger'`) - `cx()` from `class-variance-authority` to merge CSS module class with user's `className` - Spread `...props` last so consumers can override defaults +- **`data-slot` on every rendered element** — a stable, kebab-case, component-prefixed identifier (`data-slot='component-trigger'`, `data-slot='component-panel-label'`). Subparts extend the prefix; a repeated part reuses one name. Slot names are public API covered by semver — renaming one is a breaking change. Put the attribute before the `...props` spread so consumers can override. Skip `Popover.Content`/`Menu.Content` call sites (extra props land on the positioner, not the popup). ### Object.assign Composition @@ -270,6 +271,7 @@ import styles from '../.module.css'; 4. **Keyboard navigation** — Tab, Enter, Space, Arrow keys as applicable 5. **Disabled state** — `aria-disabled`, no toggle on click 6. **Sub-components** — className, ref forwarding for each sub-component +7. **data-slot contract** — a `__tests__/data-slots.test.tsx` asserting every slot renders, using `expectSlots` from `~/test-utils/data-slots` (pass `document.body` for portaled parts; also assert conditional slots disappear when their part is absent) ### Testing Tips @@ -497,6 +499,7 @@ Checklist: - [ ] All tests pass - [ ] Docs site builds and new page is generated - [ ] `displayName` set on all sub-components +- [ ] Every rendered element has a `data-slot`, with a `data-slots.test.tsx` covering them - [ ] CSS uses `--rs-*` tokens only - [ ] Export in `packages/raystack/index.tsx` in alphabetical order - [ ] Playground example added and registered diff --git a/apps/www/src/content/docs/(overview)/styling.mdx b/apps/www/src/content/docs/(overview)/styling.mdx index 1dd9b5396..22ccc77b0 100644 --- a/apps/www/src/content/docs/(overview)/styling.mdx +++ b/apps/www/src/content/docs/(overview)/styling.mdx @@ -82,6 +82,80 @@ Apsara components built on [Base UI](https://base-ui.com/) expose data attribute Common data attributes include `data-open`, `data-closed`, `data-active`, `data-disabled`, `data-state`, `data-starting-style`, and `data-ending-style`. +### With data-slot + +Every part a component renders carries a `data-slot` attribute — a stable, public identifier for that element. Use it to style inner parts that have no `className` prop of their own, without depending on Apsara's internal (hashed) class names: + +```css +/* Tint the remove button inside every filter chip */ +[data-slot="filter-chip-remove"] { + color: var(--rs-color-foreground-danger-primary); +} + +/* Make DataView loading rows taller */ +[data-slot="data-view-list-loader-row"] { + min-height: 56px; +} + +/* Hide the magnifier icon inside Search */ +[data-slot="search"] [data-slot="search-input"] { + padding-left: var(--rs-space-3); +} +``` + +Slot names follow one convention: + +- **kebab-case**, prefixed with the component name: `filter-chip-label`, `data-view-list-row`. +- Subparts extend the prefix: `data-view-filter-summary-clear`. +- The same part keeps the same name everywhere it appears (loader cells in `DataView.List` reuse `data-view-list-cell`). + +Slot names are covered by semver — renaming or removing one is a breaking change, so selectors written against them are safe to keep. This is different from state attributes like `data-open` above: state attributes tell you *how* an element currently is, `data-slot` tells you *what* it is. + +Combine them freely: + +```css +/* Only style rows of a clickable list while it is loading */ +[data-slot="data-view-list-loader-row"][aria-busy="true"] { + opacity: 0.6; +} +``` + +Slots also make reliable selectors for tests (`querySelector('[data-slot="search-clear-button"]')`) instead of reaching for hashed CSS-module classes. + +#### Styling based on what a component contains + +Because every part carries its own slot, a parent can change its own styling based on which children are present — no prop needed to signal it. CSS's `:has()` reaches into descendants: + +```css +/* A filter bar that's tighter when it has no chips yet */ +[data-slot="data-view-filters"]:has([data-slot="filter-chip"]) { + gap: var(--rs-space-4); +} + +/* Style a submit button differently only inside a form's footer */ +[data-slot="dialog-footer"] [data-slot="button"] { + width: 100%; +} +``` + +With Tailwind's arbitrary variants, the same thing reads as a utility class instead of a stylesheet rule: + +```tsx + + + +``` + +```tsx + + + +``` + +This is the same technique as the state-based selectors above (`data-[state=open]:...`) — `data-slot` just gives you a target that's stable across releases instead of one tied to internal markup. + +Coverage: every Apsara component exposes `data-slot` on every element it renders (`data-table` is the one exception — it's being phased out in favor of `DataView`). Each component's docs page lists its slot names. + ## Theming with Data Attributes The `Theme` component sets data attributes on the root `` element. Use these to conditionally style elements based on the active theme: diff --git a/apps/www/src/content/docs/components/accordion/index.mdx b/apps/www/src/content/docs/components/accordion/index.mdx index 50c56b11d..1f4621b27 100644 --- a/apps/www/src/content/docs/components/accordion/index.mdx +++ b/apps/www/src/content/docs/components/accordion/index.mdx @@ -55,6 +55,20 @@ Contains the collapsible content for an item. +### Slots + +Every rendered part carries a stable `data-slot` attribute for [styling and testing](/docs/styling#with-data-slot): + +| Slot | Element | +|------|---------| +| `accordion` | The root element that wraps all items | +| `accordion-item` | Wrapper around a single header and its panel | +| `accordion-header` | The header row that holds the trigger | +| `accordion-trigger` | The `