Skip to content
edmi-ui

Button

Pressable action control; flat by default, elevation adds sunken, raised and floating depth.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
As a link
Terminal window
npx shadcn@latest add @edmi-ui/button
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/button
Terminal window
yarn dlx shadcn@latest add @edmi-ui/button
Terminal window
bunx shadcn@latest add @edmi-ui/button

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/button
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/button
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/button
Terminal window
bunx shadcn-vue@latest add @edmi-ui/button

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/button.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/button.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/button.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/button.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import { Button } from "@/components/ui/button";
<div className="flex flex-wrap items-center gap-3">
<Button>Join index</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Delete</Button>
<Button variant="link">Link</Button>
<Button variant="brand">
Continue
<ArrowRightIcon data-icon="inline-end" />
</Button>
<Button size="sm">
<PlusIcon data-icon="inline-start" />
Create
</Button>
<Button size="icon" variant="outline" aria-label="Add">
<PlusIcon />
</Button>
</div>
<script setup lang="ts">
import { Button } from "@/components/ui/button";
</script>
<template>
<div class="flex flex-wrap items-center gap-3">
<Button>Join index</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Delete</Button>
<Button variant="link">Link</Button>
<Button variant="brand">
Continue
<ArrowRight data-icon="inline-end" />
</Button>
<Button size="sm">
<Plus data-icon="inline-start" />
Create
</Button>
<Button size="icon" variant="outline" aria-label="Add">
<Plus />
</Button>
</div>
</template>
<script lang="ts">
import { Button } from "$lib/components/ui/button";
</script>
<div class="flex flex-wrap items-center gap-3">
<Button>Join index</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Delete</Button>
<Button variant="link">Link</Button>
<Button variant="brand">Continue</Button>
<Button size="sm">Create</Button>
<Button size="icon" variant="outline" aria-label="Add">+</Button>
<Button href="#" variant="outline">As a link</Button>
</div>
registry/ui/button.tsx
"use client";
import { Button as ButtonPrimitive } from "@base-ui/react/button";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "cn";
import * as React from "react";
import {
type Elevation,
type ElevationLevel,
useElevation,
} from "@/registry/edmi/ui/elevation";
// Flat by default. `elevation` ✦ adds depth (v4): sunken -1, raised +1 (bevel), floating +2 (bevel + soft drop).
// `background-origin: border-box` keeps the gradient from repeating under the transparent border.
const face = "border-transparent [background-origin:border-box]";
// Level passed down by a ButtonGroup (items resolve: own prop, group level, scope).
const ButtonElevationContext = React.createContext<ElevationLevel | undefined>(
undefined,
);
const buttonVariants = cva(
"group/button relative inline-flex shrink-0 items-center justify-center gap-2 font-medium whitespace-nowrap transition-[filter,transform,box-shadow] outline-none select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default:
"border border-transparent bg-primary text-primary-foreground hover:bg-[color-mix(in_srgb,var(--primary)_90%,var(--background))] active:brightness-95",
secondary:
"border border-transparent bg-secondary text-secondary-foreground hover:bg-accent data-[popup-open]:bg-accent",
outline:
"border border-input bg-background text-foreground hover:bg-accent data-[popup-open]:bg-accent",
ghost: "text-foreground hover:bg-accent data-[popup-open]:bg-accent",
destructive:
"border border-transparent bg-destructive text-white hover:bg-[color-mix(in_srgb,var(--destructive)_90%,var(--background))]",
link: "px-1 text-foreground underline underline-offset-4",
// ✦ Edmi addition
brand:
"border border-transparent bg-brand text-brand-foreground hover:bg-[color-mix(in_srgb,var(--brand)_90%,var(--background))]",
},
// ✦ depth (v4)
elevation: {
flat: "",
sunken: "",
raised: "active:translate-y-px",
floating: "active:translate-y-px",
},
size: {
default:
"h-9 rounded-md px-3.5 text-[13.5px] has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
xs: "h-6 rounded-md px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
sm: "h-8 gap-1.5 rounded-[7px] px-3 text-[13px] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-[42px] rounded-lg px-5 text-[15px] has-data-[icon=inline-end]:pr-4 has-data-[icon=inline-start]:pl-4",
icon: "size-9 rounded-md",
"icon-xs":
"size-6 rounded-md in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3.5",
"icon-sm":
"size-8 rounded-[7px] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-[42px] rounded-lg",
},
},
compoundVariants: [
// +1 raised
{
elevation: "raised",
variant: "default",
class: `${face} [background-image:var(--r1-p-face)] shadow-btn-raised-primary hover:brightness-105 active:shadow-pressed`,
},
{
elevation: "raised",
variant: ["secondary", "outline", "ghost"],
class: `${face} [background-image:var(--r1-s-face)] shadow-btn-raised-neutral active:shadow-pressed`,
},
{
elevation: "raised",
variant: "destructive",
class: `${face} bg-linear-to-b from-destructive-hi to-destructive shadow-btn-raised-color hover:brightness-105 active:shadow-pressed`,
},
{
elevation: "raised",
variant: "brand",
class: `${face} bg-linear-to-b from-brand-hi to-brand shadow-btn-raised-color hover:brightness-105 active:shadow-pressed`,
},
// +2 floating (one hero action per view)
{
elevation: "floating",
variant: "default",
class: `${face} [background-image:var(--fl-p-face)] shadow-btn-float-primary active:shadow-pressed-float`,
},
{
elevation: "floating",
variant: ["secondary", "outline", "ghost"],
class: `${face} [background-image:var(--fl-s-face)] shadow-btn-float-neutral active:shadow-pressed-float`,
},
{
elevation: "floating",
variant: "destructive",
class: `${face} bg-linear-to-b from-destructive-hi to-destructive shadow-btn-float-color active:shadow-pressed-float`,
},
{
elevation: "floating",
variant: "brand",
class: `${face} bg-linear-to-b from-brand-hi to-brand shadow-btn-float-color active:shadow-pressed-float`,
},
// -1 sunken: filled variants keep their colour (8% darker) + inset, neutral ones become a well
{
elevation: "sunken",
variant: "default",
class:
"border-transparent bg-[color-mix(in_srgb,var(--primary)_92%,#000)] shadow-btn-sunken-filled",
},
{
elevation: "sunken",
variant: "destructive",
class:
"border-transparent bg-[color-mix(in_srgb,var(--destructive)_92%,#000)] shadow-btn-sunken-filled",
},
{
elevation: "sunken",
variant: "brand",
class:
"border-transparent bg-[color-mix(in_srgb,var(--brand)_92%,#000)] shadow-btn-sunken-filled",
},
{
elevation: "sunken",
variant: ["secondary", "outline", "ghost"],
class: "border-sk-bd bg-sk-bg shadow-sunken",
},
// link never gets depth
{
elevation: ["raised", "floating", "sunken"],
variant: "link",
class: "bg-none shadow-none active:translate-y-0",
},
],
defaultVariants: {
variant: "default",
size: "default",
elevation: "flat",
},
},
);
function Button({
className,
variant = "default",
size = "default",
elevation,
...props
}: ButtonPrimitive.Props &
Omit<VariantProps<typeof buttonVariants>, "elevation"> & {
/** ✦ depth: sunken -1, flat 0, raised +1, floating +2. `auto` follows the group / provider. */
elevation?: Elevation;
}) {
const group = React.useContext(ButtonElevationContext);
const own = elevation && elevation !== "auto" ? elevation : group;
const level = useElevation(
own,
variant === "default" ||
variant === "secondary" ||
variant === "destructive" ||
variant === "brand"
? "button-filled"
: "button-quiet",
);
return (
<ButtonPrimitive
data-slot="button"
data-variant={variant}
className={cn(
buttonVariants({ variant, size, elevation: level, className }),
)}
{...props}
/>
);
}
export { Button, ButtonElevationContext, buttonVariants };
registry/ui/button/Button.vue
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { ButtonVariants } from '.'
import { Primitive } from 'reka-ui'
import { computed, inject } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
import { type Elevation, useElevation } from '@/registry/edmi/ui/elevation'
import { BUTTON_ELEVATION_KEY, buttonVariants } from '.'
interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant']
size?: ButtonVariants['size']
/** ✦ depth: sunken -1, flat 0, raised +1, floating +2. `auto` follows the group / provider. */
elevation?: Elevation
class?: HTMLAttributes['class']
}
const props = withDefaults(defineProps<Props>(), {
as: 'button',
elevation: undefined,
})
const group = inject<{ readonly value: Elevation | undefined } | undefined>(BUTTON_ELEVATION_KEY, undefined)
const own = computed(() =>
props.elevation && props.elevation !== 'auto' ? props.elevation : group?.value,
)
const filledLevel = useElevation(() => own.value, 'button-filled')
const quietLevel = useElevation(() => own.value, 'button-quiet')
const level = computed(() =>
['default', 'secondary', 'destructive', 'brand'].includes(props.variant ?? 'default')
? filledLevel.value
: quietLevel.value,
)
</script>
<template>
<Primitive
data-slot="button"
:data-variant="variant ?? 'default'"
:data-size="size"
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size, elevation: level }), props.class)"
>
<slot />
</Primitive>
</template>
registry/ui/button/index.ts
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
export { default as Button } from "./Button.vue";
// Flat by default (plain shadcn look). `elevation` ✦ adds depth (v4): sunken -1, raised +1 (bevel), floating +2 (bevel + soft drop).
// `background-origin: border-box` keeps the gradient from repeating under the transparent border.
// ButtonGroup provides its level to the buttons it contains through this key (a getter object).
export const BUTTON_ELEVATION_KEY = Symbol("EDMI_BUTTON_ELEVATION");
export const buttonVariants = cva(
"group/button relative inline-flex shrink-0 items-center justify-center gap-2 font-medium whitespace-nowrap transition-[filter,transform,box-shadow] outline-none select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4",
{
variants: {
variant: {
default:
"border border-transparent bg-primary text-primary-foreground hover:bg-[color-mix(in_srgb,var(--primary)_90%,var(--background))] active:brightness-95",
secondary:
"border border-transparent bg-secondary text-secondary-foreground hover:bg-accent data-[state=open]:bg-accent",
outline:
"border border-input bg-background text-foreground hover:bg-accent data-[state=open]:bg-accent",
ghost: "text-foreground hover:bg-accent data-[state=open]:bg-accent",
destructive:
"border border-transparent bg-destructive text-white hover:bg-[color-mix(in_srgb,var(--destructive)_90%,var(--background))]",
link: "px-1 text-foreground underline underline-offset-4",
// ✦ Edmi addition
brand:
"border border-transparent bg-brand text-brand-foreground hover:bg-[color-mix(in_srgb,var(--brand)_90%,var(--background))]",
},
// ✦ depth (v4)
elevation: {
flat: "",
sunken: "",
raised: "active:translate-y-px",
floating: "active:translate-y-px",
},
size: {
default:
"h-9 rounded-md px-3.5 text-[13.5px] has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
xs: "h-6 rounded-md px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*=size-])]:size-3.5",
sm: "h-8 gap-1.5 rounded-[7px] px-3 text-[13px] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5 [&_svg:not([class*=size-])]:size-3.5",
lg: "h-[42px] rounded-lg px-5 text-[15px] has-data-[icon=inline-end]:pr-4 has-data-[icon=inline-start]:pl-4",
icon: "size-9 rounded-md",
"icon-xs":
"size-6 rounded-md in-data-[slot=button-group]:rounded-lg [&_svg:not([class*=size-])]:size-3.5",
"icon-sm":
"size-8 rounded-[7px] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-[42px] rounded-lg",
},
},
compoundVariants: [
// +1 raised
{
elevation: "raised",
variant: "default",
class:
"border-transparent [background-origin:border-box] [background-image:var(--r1-p-face)] shadow-btn-raised-primary hover:brightness-105 active:shadow-pressed",
},
{
elevation: "raised",
variant: ["secondary", "outline", "ghost"],
class:
"border-transparent [background-origin:border-box] [background-image:var(--r1-s-face)] shadow-btn-raised-neutral active:shadow-pressed",
},
{
elevation: "raised",
variant: "destructive",
class:
"border-transparent [background-origin:border-box] bg-linear-to-b from-destructive-hi to-destructive shadow-btn-raised-color hover:brightness-105 active:shadow-pressed",
},
{
elevation: "raised",
variant: "brand",
class:
"border-transparent [background-origin:border-box] bg-linear-to-b from-brand-hi to-brand shadow-btn-raised-color hover:brightness-105 active:shadow-pressed",
},
// +2 floating (one hero action per view)
{
elevation: "floating",
variant: "default",
class:
"border-transparent [background-origin:border-box] [background-image:var(--fl-p-face)] shadow-btn-float-primary active:shadow-pressed-float",
},
{
elevation: "floating",
variant: ["secondary", "outline", "ghost"],
class:
"border-transparent [background-origin:border-box] [background-image:var(--fl-s-face)] shadow-btn-float-neutral active:shadow-pressed-float",
},
{
elevation: "floating",
variant: "destructive",
class:
"border-transparent [background-origin:border-box] bg-linear-to-b from-destructive-hi to-destructive shadow-btn-float-color active:shadow-pressed-float",
},
{
elevation: "floating",
variant: "brand",
class:
"border-transparent [background-origin:border-box] bg-linear-to-b from-brand-hi to-brand shadow-btn-float-color active:shadow-pressed-float",
},
// -1 sunken: filled variants keep their colour (8% darker) + inset, neutral ones become a well
{
elevation: "sunken",
variant: "default",
class:
"border-transparent bg-[color-mix(in_srgb,var(--primary)_92%,#000)] shadow-btn-sunken-filled",
},
{
elevation: "sunken",
variant: "destructive",
class:
"border-transparent bg-[color-mix(in_srgb,var(--destructive)_92%,#000)] shadow-btn-sunken-filled",
},
{
elevation: "sunken",
variant: "brand",
class:
"border-transparent bg-[color-mix(in_srgb,var(--brand)_92%,#000)] shadow-btn-sunken-filled",
},
{
elevation: "sunken",
variant: ["secondary", "outline", "ghost"],
class: "border-sk-bd bg-sk-bg shadow-sunken",
},
// link never gets depth
{
elevation: ["raised", "floating", "sunken"],
variant: "link",
class: "bg-none shadow-none active:translate-y-0",
},
],
defaultVariants: {
variant: "default",
size: "default",
elevation: "flat",
},
},
);
export type ButtonVariants = VariantProps<typeof buttonVariants>;
src/lib/registry/ui/button/button.svelte
<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";
import type { Elevation } from "$lib/registry/ui/elevation/index.js";
// Flat by default. `elevation` ✦ adds depth (v4): sunken -1, raised +1 (bevel), floating +2 (bevel + soft drop).
// `background-origin: border-box` keeps the gradient from repeating under the transparent border.
// ButtonGroup provides its level to the buttons it contains through this key (a getter).
export const BUTTON_ELEVATION_CONTEXT = Symbol("EDMI_BUTTON_ELEVATION");
export const buttonVariants = tv({
base: "group/button relative inline-flex shrink-0 items-center justify-center gap-2 font-medium whitespace-nowrap transition-[filter,transform,box-shadow] outline-none select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
variants: {
variant: {
default: "border border-transparent bg-primary text-primary-foreground hover:bg-[color-mix(in_srgb,var(--primary)_90%,var(--background))] active:brightness-95",
secondary: "border border-transparent bg-secondary text-secondary-foreground hover:bg-accent aria-expanded:bg-accent",
outline: "border border-input bg-background text-foreground hover:bg-accent aria-expanded:bg-accent",
ghost: "text-foreground hover:bg-accent aria-expanded:bg-accent",
destructive: "border border-transparent bg-destructive text-white hover:bg-[color-mix(in_srgb,var(--destructive)_90%,var(--background))]",
link: "px-1 text-foreground underline underline-offset-4",
// ✦ Edmi addition
brand: "border border-transparent bg-brand text-brand-foreground hover:bg-[color-mix(in_srgb,var(--brand)_90%,var(--background))]",
},
// ✦ depth (v4)
elevation: {
flat: "",
sunken: "",
raised: "active:translate-y-px",
floating: "active:translate-y-px",
},
size: {
default: "h-9 rounded-md px-3.5 text-[13.5px] has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
xs: "h-6 rounded-md px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
sm: "h-8 gap-1.5 rounded-[7px] px-3 text-[13px] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-[42px] rounded-lg px-5 text-[15px] has-data-[icon=inline-end]:pr-4 has-data-[icon=inline-start]:pl-4",
icon: "size-9 rounded-md",
"icon-xs": "size-6 rounded-md in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3.5",
"icon-sm": "size-8 rounded-[7px] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-[42px] rounded-lg",
},
},
compoundVariants: [
// +1 raised
{
elevation: "raised",
variant: "default",
class: "border-transparent [background-origin:border-box] [background-image:var(--r1-p-face)] shadow-btn-raised-primary hover:brightness-105 active:shadow-pressed",
},
{
elevation: "raised",
variant: ["secondary", "outline", "ghost"],
class: "border-transparent [background-origin:border-box] [background-image:var(--r1-s-face)] shadow-btn-raised-neutral active:shadow-pressed",
},
{
elevation: "raised",
variant: "destructive",
class: "border-transparent [background-origin:border-box] bg-linear-to-b from-destructive-hi to-destructive shadow-btn-raised-color hover:brightness-105 active:shadow-pressed",
},
{
elevation: "raised",
variant: "brand",
class: "border-transparent [background-origin:border-box] bg-linear-to-b from-brand-hi to-brand shadow-btn-raised-color hover:brightness-105 active:shadow-pressed",
},
// +2 floating (one hero action per view)
{
elevation: "floating",
variant: "default",
class: "border-transparent [background-origin:border-box] [background-image:var(--fl-p-face)] shadow-btn-float-primary active:shadow-pressed-float",
},
{
elevation: "floating",
variant: ["secondary", "outline", "ghost"],
class: "border-transparent [background-origin:border-box] [background-image:var(--fl-s-face)] shadow-btn-float-neutral active:shadow-pressed-float",
},
{
elevation: "floating",
variant: "destructive",
class: "border-transparent [background-origin:border-box] bg-linear-to-b from-destructive-hi to-destructive shadow-btn-float-color active:shadow-pressed-float",
},
{
elevation: "floating",
variant: "brand",
class: "border-transparent [background-origin:border-box] bg-linear-to-b from-brand-hi to-brand shadow-btn-float-color active:shadow-pressed-float",
},
// -1 sunken: filled variants keep their colour (8% darker) + inset, neutral ones become a well
{
elevation: "sunken",
variant: "default",
class:
"border-transparent bg-[color-mix(in_srgb,var(--primary)_92%,#000)] shadow-btn-sunken-filled",
},
{
elevation: "sunken",
variant: "destructive",
class:
"border-transparent bg-[color-mix(in_srgb,var(--destructive)_92%,#000)] shadow-btn-sunken-filled",
},
{
elevation: "sunken",
variant: "brand",
class:
"border-transparent bg-[color-mix(in_srgb,var(--brand)_92%,#000)] shadow-btn-sunken-filled",
},
{
elevation: "sunken",
variant: ["secondary", "outline", "ghost"],
class: "border-sk-bd bg-sk-bg shadow-sunken",
},
// link never gets depth
{
elevation: ["raised", "floating", "sunken"],
variant: "link",
class: "bg-none shadow-none active:translate-y-0",
},
],
defaultVariants: {
variant: "default",
size: "default",
elevation: "flat",
},
});
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;
/** ✦ depth: sunken -1, flat 0, raised +1, floating +2. `auto` follows the group / provider (link never gets depth). */
elevation?: Elevation;
};
</script>
<script lang="ts">
import { getContext } from "svelte";
import { useElevation } from "$lib/registry/ui/elevation/index.js";
let {
class: className,
variant = "default",
size = "default",
elevation = "auto",
ref = $bindable(null),
href = undefined,
type = "button",
disabled,
children,
...restProps
}: ButtonProps = $props();
const group = getContext<(() => Elevation | undefined) | undefined>(BUTTON_ELEVATION_CONTEXT);
const own = $derived(elevation && elevation !== "auto" ? elevation : group?.());
const filledLevel = useElevation(() => own, "button-filled");
const quietLevel = useElevation(() => own, "button-quiet");
const level = $derived(
["default", "secondary", "destructive", "brand"].includes(variant ?? "default")
? filledLevel.current
: quietLevel.current
);
</script>
{#if href}
<a
bind:this={ref}
data-slot="button"
data-variant={variant}
class={cn(buttonVariants({ variant, size, elevation: level }), 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"
data-variant={variant}
class={cn(buttonVariants({ variant, size, elevation: level }), className)}
{type}
{disabled}
{...restProps}
>
{@render children?.()}
</button>
{/if}
src/lib/registry/ui/button/index.ts
import Root, {
BUTTON_ELEVATION_CONTEXT,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
export {
BUTTON_ELEVATION_CONTEXT,
type ButtonProps as Props,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
Root,
//
Root as Button,
};

See the Elevation guide for the levels, roles and layered mode.

Flat by default. elevation adds depth: raised (+1) is a bevel (inner rim, top highlight, dark hairline), floating (+2) adds one soft drop for a hero action, sunken (-1) presses the button into a well (filled variants keep their colour, 8% darker). ghost follows the neutral faces; link never gets depth. Pressed sinks 1px. auto follows the nearest ElevationProvider (layered mode: filled variants resolve to raised).

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Flat (0)

Raised (+1)

Floating (+2)

Sunken (-1)

Flat (0)

Raised (+1)

Floating (+2)

Sunken (-1)

Flat (0)

Raised (+1)

Floating (+2)

Sunken (-1)

Prop Type Default
variant default | secondary | outline | ghost | destructive | link | brand default
size xs | sm | default | lg | icon-xs | icon-sm | icon | icon-lg default
render ReactElement none
elevation "auto" | "sunken" | "flat" | "raised" | "floating" "auto"
  • brand variant (green).
  • elevation prop: a bevel (no hard lip) that sinks 1px on press. link ignores it; ghost follows the neutral faces.
  • Style links with buttonVariants() on a plain <a>.