Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions apps/www/src/app/examples/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
Select,
Sidebar,
Spinner,
Tabs,
Text,
TextArea,
Tooltip
Expand Down Expand Up @@ -132,6 +133,29 @@ const Page = () => {
<Text size='large' weight='medium' style={{ marginBottom: '24px' }}>
Main
</Text>

<Text
size='large'
weight='medium'
style={{ marginTop: '0', marginBottom: '16px' }}
>
Tabs Examples
</Text>
<Flex direction='column' gap={6} style={{ marginBottom: '32px' }}>
<Tabs defaultValue='tab1'>
<Tabs.List>
<Tabs.Tab value='tab1'>Account</Tabs.Tab>
<Tabs.Tab value='tab2' disabled>
Password
</Tabs.Tab>
<Tabs.Tab value='tab3'>Settings</Tabs.Tab>
</Tabs.List>
<Tabs.Content value='tab1'>Account settings</Tabs.Content>
<Tabs.Content value='tab2'>Password settings</Tabs.Content>
<Tabs.Content value='tab3'>Other settings</Tabs.Content>
</Tabs>
</Flex>

<code
style={{
fontFamily: 'var(--rs-font-mono)',
Expand Down
2 changes: 2 additions & 0 deletions apps/www/src/content/docs/components/tabs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,5 @@ Renders the content panel for a tab.
- Supports keyboard navigation with arrow keys between tabs
- Uses `role="tablist"`, `role="tab"`, and `role="tabpanel"`
- Active tab is indicated with `aria-selected`
- Leading icon is wrapped with `aria-hidden` so it is not announced (treated as decorative)
- Respects `prefers-reduced-motion: reduce`: the tab indicator does not animate when the user has requested reduced motion in OS/browser settings
2 changes: 1 addition & 1 deletion apps/www/src/content/docs/components/tabs/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface TabsTabProps {
/** Unique identifier for the tab. */
value: any;

/** Optional icon element to display before the label. */
/** Optional icon element to display before the label. Rendered in a wrapper with aria-hidden so it is not announced by screen readers (decorative). */
leadingIcon?: React.ReactNode;

/** Whether the tab is disabled. */
Expand Down
17 changes: 17 additions & 0 deletions packages/raystack/components/tabs/__tests__/tabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,23 @@ describe('Tabs', () => {
expect(iconWrapper).toBeInTheDocument();
expect(iconWrapper).toContainElement(screen.getByTestId('tab-icon'));
});

it('sets aria-hidden on leadingIcon wrapper for accessibility', () => {
const icon = <span data-testid='tab-icon'>📁</span>;
const { container } = render(
<Tabs defaultValue='tab1'>
<Tabs.List>
<Tabs.Tab value='tab1' leadingIcon={icon}>
{TAB_1_TEXT}
</Tabs.Tab>
</Tabs.List>
<Tabs.Content value='tab1'>{CONTENT_1_TEXT}</Tabs.Content>
</Tabs>
);

const iconWrapper = container.querySelector(`.${styles['trigger-icon']}`);
expect(iconWrapper).toHaveAttribute('aria-hidden', 'true');
});
});

describe('Indicator', () => {
Expand Down
16 changes: 13 additions & 3 deletions packages/raystack/components/tabs/tabs.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,15 @@
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
height: 28px;
height: var(--rs-space-8);
line-height: var(--rs-line-height-small);
letter-spacing: var(--rs-letter-spacing-small);
box-sizing: border-box;
position: relative;
z-index: 1;
}

/* Hover state on trigger */
.trigger:hover:not([data-disabled]) {
color: var(--rs-color-foreground-base-primary);
}
Expand All @@ -60,8 +61,8 @@

.trigger-icon {
display: inline-flex;
width: 16px;
height: 16px;
width: var(--rs-space-5);
height: var(--rs-space-5);
color: currentColor;
justify-content: center;
align-items: center;
Expand All @@ -85,6 +86,15 @@
z-index: 0;
}

/* Media query that matches when the user has
asked the OS/browser for less or no motion
(e.g. “Reduce motion” in accessibility settings). */
@media (prefers-reduced-motion: reduce) {
.indicator {
transition: none;
}
}

.content {
outline: none;
}
Expand Down
4 changes: 3 additions & 1 deletion packages/raystack/components/tabs/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ const TabsTab = forwardRef<HTMLButtonElement, TabsTabProps>(
{...props}
>
{leadingIcon && (
<span className={styles['trigger-icon']}>{leadingIcon}</span>
<span className={styles['trigger-icon']} aria-hidden>
{leadingIcon}
</span>
)}
{children}
</TabsPrimitive.Tab>
Expand Down