shadcn-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>