About shadcn-svelte Components
A coding skill for developers building interfaces with shadcn-svelte and bits-ui. It sets out component patterns for triggers, dialogs, menus, styling, and navigation.
Key features
- 01Component import patterns
- 02Child snippet trigger pattern
- 03Dialog naming conventions
- 04Dropdown option organization
- 05Class and navigation guidance
Use cases
- 01Build accessible menu triggers
- 02Name dialog state consistently
- 03Organize dropdown menu options
- 04Add slide-out panels to interfaces
MCP support
The README mentions using context7 for API reference but does not describe how this skill interacts with MCP.
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ exceptionless/ exceptionless --skill shadcn-svelte-componentsAfter installing, type/shadcn-svelte-componentsin Claude Code to confirm it loaded.
Details
From the project READMEshadcn-svelte Components
Documentation: shadcn-svelte.com | Use
context7for API reference
Use shadcn-svelte components (bits-ui) for UI. Import with namespace pattern.
Import Pattern
<script lang="ts">
import * as Dialog from '$comp/ui/dialog';
import * as DropdownMenu from '$comp/ui/dropdown-menu';
import * as Tooltip from '$comp/ui/tooltip';
import { Button } from '$comp/ui/button';
import { Input } from '$comp/ui/input';
</script>
Trigger Components - Child Snippet Pattern
When using trigger components with custom elements like Button, always use the child snippet pattern:
<!-- ✅ Correct: Single tab stop, proper accessibility -->
<Tooltip.Root>
<Tooltip.Trigger>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon">
<Icon />
</Button>
{/snippet}
</Tooltip.Trigger>
<Tooltip.Content>Tooltip text</Tooltip.Content>
</Tooltip.Root>
Why This Pattern?
- Single Tab Stop: Creates only one focusable element
- Proper Props Delegation: ARIA attributes pass through correctly
- Accessibility: Maintains keyboard navigation
- Official Pattern: Documented shadcn-svelte/bits-ui pattern
Wrong Patterns
<!-- ❌ Wrong: Creates two focusable elements (double-tab issue) -->
<Tooltip.Trigger>
<Button>Content</Button>
</Tooltip.Trigger>
<!-- ❌ Wrong: Manual styling replicates button styles -->
<Tooltip.Trigger class="hover:bg-accent inline-flex...">
<Icon />
</Tooltip.Trigger>
Apply to All Triggers
<!-- DropdownMenu -->
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} variant="outline">
Open Menu
<ChevronDown />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<!-- Popover -->
<Popover.Trigger>
{#snippet child({ props })}
<Button {...props} variant="outline" class="w-70">
Select Date
<CalendarIcon />
</Button>
{/snippet}
</Popover.Trigger>
<!-- Dialog -->
<Dialog.Trigger>
{#snippet child({ props })}
<Button {...props}>Open Dialog</Button>
{/snippet}
</Dialog.Trigger>
Dialog Pattern
<script lang="ts">
import * as Dialog from '$comp/ui/dialog';
import { Button } from '$comp/ui/button';
let openCreateDialog = $state(false);
</script>
<Button onclick={() => (openCreateDialog = true)}>Create</Button>
{#if openCreateDialog}
<Dialog.Root bind:open={openCreateDialog}>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Create Organization</Dialog.Title>
<Dialog.Description>
Add a new organization to your account.
</Dialog.Description>
</Dialog.Header>
<!-- Form content -->
<Dialog.Footer>
<Button variant="outline" onclick={() => (openCreateDialog = false)}>
Cancel
</Button>
<Button type="submit">Create</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
{/if}
Dialog Naming Convention
- Use
open[ComponentName]Dialogpattern - Avoid generic names like
showDialogorisOpen
<script lang="ts">
let openSuspendOrganizationDialog = $state(false);
let openMarkStackDiscardedDialog = $state(false);
let openInviteUserDialog = $state(false);
</script>
DropdownMenu with Options
<script lang="ts">
import * as DropdownMenu from '$comp/ui/dropdown-menu';
import { statusOptions } from './options';
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} variant="outline">
Select Status
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{#each statusOptions as option}
<DropdownMenu.Item onclick={() => handleSelect(option.value)}>
{option.label}
</DropdownMenu.Item>
{/each}
</DropdownMenu.Content>
</DropdownMenu.Root>
Options File Pattern
// options.ts
import type { DropdownItem } from '$shared/types';
export enum Status {
Active = 'active',
Inactive = 'inactive',
Pending = 'pending'
}
export const statusOptions: DropdownItem<Status>[] = [
{ value: Status.Active, label: 'Active' },
{ value: Status.Inactive, label: 'Inactive' },
{ value: Status.Pending, label: 'Pending' }
];
Sheet (Slide-out Panel)
<Sheet.Root bind:open={openFiltersSheet}>
<Sheet.Content side="right">
<Sheet.Header>
<Sheet.Title>Filters</Sheet.Title>
</Sheet.Header>
<!-- Filter controls -->
<Sheet.Footer>
<Button onclick={applyFilters}>Apply</Button>
</Sheet.Footer>
</Sheet.Content>
</Sheet.Root>
Class Merging with Array Syntax
Use Svelte array syntax for conditional classes (NOT cn utility):
<!-- ✅ Preferred: Array syntax -->
<Button class={['w-full', isActive && 'bg-primary']}>
Click me
</Button>
<div class={['flex items-center', expanded && 'bg-muted', className]}>
Content
</div>
<!-- ❌ Avoid: cn utility (older pattern) -->
<Button class={cn('w-full', isActive && 'bg-primary')}>
Navigation Preference
Prefer href navigation over onclick/goto:
<!-- ✅ Preferred: Native navigation -->
<Button href="/organizations/new">Create</Button>
<!-- Use onclick only when navigation logic required -->
<Button onclick={async () => {
await saveData();
goto('/success');
}}>
Save and Continue
</Button>