Button
Pressable action control; flat by default, elevation adds sunken, raised and floating depth.
import { Button } from "@/components/ui/button";import { IconPlaceholder } from "@/edmi/icon-placeholder";
export default function Demo() { return ( <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 <IconPlaceholder lucide="ArrowRightIcon" tabler="IconArrowRight" hugeicons="ArrowRight02Icon" phosphor="ArrowRightIcon" remixicon="RiArrowRightLine" data-icon="inline-end" /> </Button> <Button size="sm"> <IconPlaceholder lucide="PlusIcon" tabler="IconPlus" hugeicons="PlusSignIcon" phosphor="PlusIcon" remixicon="RiAddLine" data-icon="inline-start" /> Create </Button> <Button size="icon" variant="outline" aria-label="Add"> <IconPlaceholder lucide="PlusIcon" tabler="IconPlus" hugeicons="PlusSignIcon" phosphor="PlusIcon" remixicon="RiAddLine" /> </Button> </div> );}<script setup lang="ts">import { ArrowRight, Plus } from "@lucide/vue";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";
let count = $state(0);</script>
<div class="flex flex-wrap items-center gap-3"> <Button onclick={() => count++}>Join index{count ? ` (${count})` : ""}</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>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/buttonpnpm dlx shadcn@latest add @edmi-ui/buttonyarn dlx shadcn@latest add @edmi-ui/buttonbunx shadcn@latest add @edmi-ui/buttonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @base-ui/react class-variance-authorityTerminal window pnpm add @base-ui/react class-variance-authorityTerminal window yarn add @base-ui/react class-variance-authorityTerminal window bun add @base-ui/react class-variance-authorityAdd the Edmi components this one builds on:
elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/buttonpnpm dlx shadcn-vue@latest add @edmi-ui/buttonyarn dlx shadcn-vue@latest add @edmi-ui/buttonbunx shadcn-vue@latest add @edmi-ui/buttonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install reka-ui class-variance-authorityTerminal window pnpm add reka-ui class-variance-authorityTerminal window yarn add reka-ui class-variance-authorityTerminal window bun add reka-ui class-variance-authorityAdd the Edmi components this one builds on:
elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/button.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/button.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/button.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/button.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
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>Component source
Section titled “Component source”"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 };<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>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>;<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}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,};Elevation ✦
Section titled “Elevation ✦”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).
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
import { Button } from "@/components/ui/button";
const variants = [ "default", "secondary", "outline", "destructive", "brand", "ghost",] as const;
const levels = [ { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }, { value: "sunken", label: "Sunken (-1)" },] as const;
export default function Demo() { return ( <div className="flex flex-col gap-5"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <div className="flex flex-wrap items-center gap-3"> {variants.map((variant) => ( <Button key={variant} variant={variant} elevation={value}> {variant} </Button> ))} <Button variant="link" elevation={value}> link </Button> </div> </div> ))} </div> );}<script setup lang="ts">import { Button } from "@/components/ui/button";
const variants = ["default", "secondary", "outline", "destructive", "brand", "ghost"] as const;
const levels = [{ value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }, { value: "sunken", label: "Sunken (-1)" }];</script>
<template> <div class="flex flex-col gap-5"> <div v-for="level in levels" :key="level.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ level.label }}</p> <div class="flex flex-wrap items-center gap-3"> <Button v-for="variant in variants" :key="variant" :variant="variant" :elevation="level.value">{{ variant }}</Button> <Button variant="link" :elevation="level.value">link</Button> </div> </div> </div></template><script lang="ts"> import { Button } from "$lib/components/ui/button";
const variants = ["default", "secondary", "outline", "destructive", "brand", "ghost"] as const;
const levels = [{ value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }, { value: "sunken", label: "Sunken (-1)" }] as const;</script>
<div class="flex flex-col gap-5"> {#each levels as level (level.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{level.label}</p> <div class="flex flex-wrap items-center gap-3"> {#each variants as variant (variant)} <Button {variant} elevation={level.value}>{variant}</Button> {/each} <Button variant="link" elevation={level.value}>link</Button> </div> </div> {/each}</div>| 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" |
✦ Edmi additions
Section titled “✦ Edmi additions”brandvariant (green).elevationprop: a bevel (no hard lip) that sinks 1px on press.linkignores it;ghostfollows the neutral faces.- Style links with
buttonVariants()on a plain<a>.