Epicenter
Local-first, open source apps
<script lang="ts">
import ArrowUpIcon from "@lucide/svelte/icons/arrow-up";
import { Button } from "$lib/components/ui/button/index.js";
</script>
<div class="flex flex-wrap items-center gap-2 md:flex-row">
<Button variant="outline">Button</Button>
<Button variant="outline" size="icon" aria-label="Submit">
<ArrowUpIcon />
</Button>
</div> Installation #
pnpm dlx shadcn-svelte@latest add buttonnpx shadcn-svelte@latest add buttonnpx shadcn-svelte@latest add buttonbun x shadcn-svelte@latest add buttonCopy and paste the following code into your project.
<script lang="ts" module>
import { type VariantProps, tv } from "tailwind-variants";
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
export const buttonVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 active:not-aria-[haspopup]:translate-y-px [&_svg:not([class*='size-'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80",
outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground shadow-xs",
secondary: "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5",
lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
icon: "size-9",
"icon-xs": "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
</script>
<script lang="ts">
let {
class: className,
variant = "default",
size = "default",
ref = $bindable(null),
href = undefined,
type = "button",
disabled,
children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<a
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
href={disabled ? undefined : href}
aria-disabled={disabled}
role={disabled ? "link" : undefined}
tabindex={disabled ? -1 : undefined}
{...restProps}
>
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
{type}
{disabled}
{...restProps}
>
{@render children?.()}
</button>
{/if}
Usage #
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";
</script>
<Button variant="outline">Button</Button> Cursor #
Tailwind v4 switched from cursor: pointer to cursor: default for the button component.
If you want to keep the cursor: pointer behavior, add the following code to your CSS file:
@layer base {
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
} Size #
Use the size prop to change the size of the button.
<script lang="ts">
import ArrowUpRightIcon from "@lucide/svelte/icons/arrow-up-right";
import { Button } from "$lib/components/ui/button/index.js";
</script>
<div class="flex flex-col items-start gap-8 sm:flex-row">
<div class="flex items-start gap-2">
<Button size="xs" variant="outline">Extra Small</Button>
<Button size="icon-xs" aria-label="Submit" variant="outline">
<ArrowUpRightIcon />
</Button>
</div>
<div class="flex items-start gap-2">
<Button size="sm" variant="outline">Small</Button>
<Button size="icon-sm" aria-label="Submit" variant="outline">
<ArrowUpRightIcon />
</Button>
</div>
<div class="flex items-start gap-2">
<Button variant="outline">Default</Button>
<Button size="icon" aria-label="Submit" variant="outline">
<ArrowUpRightIcon />
</Button>
</div>
<div class="flex items-start gap-2">
<Button variant="outline" size="lg">Large</Button>
<Button size="icon-lg" aria-label="Submit" variant="outline">
<ArrowUpRightIcon />
</Button>
</div>
</div> Default #
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";
</script>
<Button>Button</Button> Outline #
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";
</script>
<Button variant="outline">Outline</Button> Secondary #
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";
</script>
<Button variant="secondary">Secondary</Button> Ghost #
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";
</script>
<Button variant="ghost">Ghost</Button> Destructive #
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";
</script>
<Button variant="destructive">Destructive</Button> Link #
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";
</script>
<Button variant="link">Link</Button> Icon #
<script lang="ts">
import CircleFadingArrowUpIcon from "@lucide/svelte/icons/circle-fading-arrow-up";
import { Button } from "$lib/components/ui/button/index.js";
</script>
<Button variant="outline" size="icon" aria-label="Submit">
<CircleFadingArrowUpIcon />
</Button> With Icon #
Remember to add the data-icon="inline-start" or data-icon="inline-end" attribute to the icon for the correct spacing.
<script lang="ts">
import IconGitBranch from "@tabler/icons-svelte/icons/git-branch";
import IconGitFork from "@tabler/icons-svelte/icons/git-fork";
import { Button } from "$lib/components/ui/button/index.js";
</script>
<div class="flex gap-2">
<Button variant="outline">
<IconGitBranch data-icon="inline-start" /> New Branch
</Button>
<Button variant="outline">
Fork
<IconGitFork data-icon="inline-end" />
</Button>
</div> Rounded #
Use the rounded-full class to make the button rounded.
<script lang="ts">
import ArrowUpIcon from "@lucide/svelte/icons/arrow-up";
import { Button } from "$lib/components/ui/button/index.js";
</script>
<div class="flex gap-2">
<Button class="rounded-full">Get Started</Button>
<Button variant="outline" size="icon" class="rounded-full">
<ArrowUpIcon />
</Button>
</div> Spinner #
Render a <Spinner /> component inside the button to show a loading state. Remember to add the data-icon="inline-start" or data-icon="inline-end" attribute to the spinner for the correct spacing.
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";
import { Spinner } from "$lib/components/ui/spinner/index.js";
</script>
<div class="flex gap-2">
<Button variant="outline" disabled>
<Spinner data-icon="inline-start" />
Generating
</Button>
<Button variant="secondary" disabled>
Downloading
<Spinner data-icon="inline-end" />
</Button>
</div> Button Group #
To create a button group, use the ButtonGroup component. See the Button Group documentation for more details.
<script lang="ts">
import Archive from "@lucide/svelte/icons/archive";
import ArrowLeft from "@lucide/svelte/icons/arrow-left";
import CalendarPlus from "@lucide/svelte/icons/calendar-plus";
import Clock from "@lucide/svelte/icons/clock";
import ListFilter from "@lucide/svelte/icons/list-filter";
import MailCheck from "@lucide/svelte/icons/mail-check";
import MoreHorizontal from "@lucide/svelte/icons/more-horizontal";
import Tag from "@lucide/svelte/icons/tag";
import Trash2 from "@lucide/svelte/icons/trash-2";
import * as ButtonGroup from "$lib/components/ui/button-group/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import { Button } from "$lib/components/ui/button/index.js";
let label = $state("personal");
</script>
<ButtonGroup.Root>
<ButtonGroup.Root class="hidden sm:flex">
<Button variant="outline" size="icon" aria-label="Go Back">
<ArrowLeft />
</Button>
</ButtonGroup.Root>
<ButtonGroup.Root>
<Button variant="outline">Archive</Button>
<Button variant="outline">Report</Button>
</ButtonGroup.Root>
<ButtonGroup.Root>
<Button variant="outline">Snooze</Button>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button
{...props}
variant="outline"
size="icon"
aria-label="More Options"
>
<MoreHorizontal />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end" class="w-40">
<DropdownMenu.Group>
<DropdownMenu.Item>
<MailCheck />
Mark as Read
</DropdownMenu.Item>
<DropdownMenu.Item>
<Archive />
Archive
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Group>
<DropdownMenu.Item>
<Clock />
Snooze
</DropdownMenu.Item>
<DropdownMenu.Item>
<CalendarPlus />
Add to Calendar
</DropdownMenu.Item>
<DropdownMenu.Item>
<ListFilter />
Add to List
</DropdownMenu.Item>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger>
<Tag />
Label As...
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent>
<DropdownMenu.RadioGroup bind:value={label}>
<DropdownMenu.RadioItem value="personal"
>Personal</DropdownMenu.RadioItem
>
<DropdownMenu.RadioItem value="work"
>Work</DropdownMenu.RadioItem
>
<DropdownMenu.RadioItem value="other"
>Other</DropdownMenu.RadioItem
>
</DropdownMenu.RadioGroup>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Group>
<DropdownMenu.Item variant="destructive">
<Trash2 />
Trash
</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
</ButtonGroup.Root>
</ButtonGroup.Root> As Link #
Pass an href prop to <Button /> to render it as an <a> element that looks like a button. You can also use the buttonVariants helper to make any other element look like a button.
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";
</script>
<Button href="#" variant="secondary" size="sm">Login</Button>