March 2024 Introducing Blocks Blocks are ready-made components that you can use to build your apps. They are fully responsive, accessible, and composable, meaning they are built using the same principles as the rest of the components in shadcn-svelte.
v0 only supports React at the moment, so you can't customize them like you can the original from shadcn/ui . However, if having support for Svelte interests you, I'm sure the v0 team would love to hear about it. :)
New Component: Breadcrumb We've added a new component to the project, Breadcrumb .
< script lang = "ts" >
import * as Breadcrumb from "$lib/components/ui/breadcrumb/index.js" ;
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js" ;
</ script >
< Breadcrumb . Root >
< Breadcrumb . List >
< Breadcrumb . Item >
< Breadcrumb . Link href = "/" >Home</ Breadcrumb . Link >
</ Breadcrumb . Item >
< Breadcrumb . Separator />
< Breadcrumb . Item >
< DropdownMenu . Root >
< DropdownMenu . Trigger class = "flex items-center gap-1" >
< Breadcrumb . Ellipsis class = "size-4" />
< span class = "sr-only" >Toggle menu</ span >
</ DropdownMenu . Trigger >
< DropdownMenu . Content align = "start" >
< DropdownMenu . Item >Documentation</ DropdownMenu . Item >
< DropdownMenu . Item >Themes</ DropdownMenu . Item >
< DropdownMenu . Item >GitHub</ DropdownMenu . Item >
</ DropdownMenu . Content >
</ DropdownMenu . Root >
</ Breadcrumb . Item >
< Breadcrumb . Separator />
< Breadcrumb . Item >
< Breadcrumb . Link href = "/docs/components" >Components</ Breadcrumb . Link >
</ Breadcrumb . Item >
< Breadcrumb . Separator />
< Breadcrumb . Item >
< Breadcrumb . Page >Breadcrumb</ Breadcrumb . Page >
</ Breadcrumb . Item >
</ Breadcrumb . List >
</ Breadcrumb . Root >
Copy
< script lang = "ts" >
import * as Breadcrumb from "$lib/components/ui/breadcrumb/index.js" ;
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js" ;
</ script >
< Breadcrumb . Root >
< Breadcrumb . List >
< Breadcrumb . Item >
< Breadcrumb . Link href = "/" >Home</ Breadcrumb . Link >
</ Breadcrumb . Item >
< Breadcrumb . Separator />
< Breadcrumb . Item >
< DropdownMenu . Root >
< DropdownMenu . Trigger class = "flex items-center gap-1" >
< Breadcrumb . Ellipsis class = "size-4" />
< span class = "sr-only" >Toggle menu</ span >
</ DropdownMenu . Trigger >
< DropdownMenu . Content align = "start" >
< DropdownMenu . Item >Documentation</ DropdownMenu . Item >
< DropdownMenu . Item >Themes</ DropdownMenu . Item >
< DropdownMenu . Item >GitHub</ DropdownMenu . Item >
</ DropdownMenu . Content >
</ DropdownMenu . Root >
</ Breadcrumb . Item >
< Breadcrumb . Separator />
< Breadcrumb . Item >
< Breadcrumb . Link href = "/docs/components" >Components</ Breadcrumb . Link >
</ Breadcrumb . Item >
< Breadcrumb . Separator />
< Breadcrumb . Item >
< Breadcrumb . Page >Breadcrumb</ Breadcrumb . Page >
</ Breadcrumb . Item >
</ Breadcrumb . List >
</ Breadcrumb . Root >
Copy
We've added a new component to the project, Scroll Area , which is built on top of the Scroll Area component from Bits UI.
It supports both vertical and horizontal scrolling, and is designed to provide a consistent experience across all browsers and platforms.
Examples
< script lang = "ts" >
import { ScrollArea } from "$lib/components/ui/scroll-area/index.js" ;
import { Separator } from "$lib/components/ui/separator/index.js" ;
const tags = Array. from ({ length: 50 }). map (
( _ , i , a ) => `v1.2.0-beta.${ a . length - i }`
);
</ script >
< ScrollArea class = "h-72 w-48 rounded-md border" >
< div class = "p-4" >
< h4 class = "mb-4 text-sm font-medium leading-none" >Tags</ h4 >
{# each tags as tag}
< div class = "text-sm" >
{tag}
</ div >
< Separator class = "my-2" />
{/ each }
</ div >
</ ScrollArea >
Copy
< script lang = "ts" >
import { ScrollArea } from "$lib/components/ui/scroll-area/index.js" ;
import { Separator } from "$lib/components/ui/separator/index.js" ;
const tags = Array. from ({ length: 50 }). map (
( _ , i , a ) => `v1.2.0-beta.${ a . length - i }`
);
</ script >
< ScrollArea class = "h-72 w-48 rounded-md border" >
< div class = "p-4" >
< h4 class = "mb-4 text-sm font-medium leading-none" >Tags</ h4 >
{# each tags as tag}
< div class = "text-sm" >
{tag}
</ div >
< Separator class = "my-2" />
{/ each }
</ div >
</ ScrollArea >
Copy
< script lang = "ts" >
import { ScrollArea } from "$lib/components/ui/scroll-area/index.js" ;
type Artwork = {
artist : string ;
art : string ;
};
const works : Artwork [] = [
{
artist: "Ornella Binni" ,
art: "https://images.unsplash.com/photo-1465869185982-5a1a7522cbcb?auto=format&fit=crop&w=300&q=80"
},
{
artist: "Tom Byrom" ,
art: "https://images.unsplash.com/photo-1548516173-3cabfa4607e9?auto=format&fit=crop&w=300&q=80"
},
{
artist: "Vladimir Malyavko" ,
art: "https://images.unsplash.com/photo-1494337480532-3725c85fd2ab?auto=format&fit=crop&w=300&q=80"
}
];
</ script >
< ScrollArea
class = "w-96 whitespace-nowrap rounded-md border"
orientation = "horizontal"
>
< div class = "flex w-max space-x-4 p-4" >
{# each works as artwork (artwork.artist)}
< figure class = "shrink-0" >
< div class = "overflow-hidden rounded-md" >
< img
src ={artwork.art}
alt ={ `Photo by ${ artwork . artist }` }
class = "aspect-[3/4] h-fit w-fit object-cover"
width ={ 300 }
height ={ 400 }
/>
</ div >
< figcaption class = "text-muted-foreground pt-2 text-xs" >
Photo by
< span class = "text-foreground font-semibold" >
{artwork.artist}
</ span >
</ figcaption >
</ figure >
{/ each }
</ div >
</ ScrollArea >
Copy
< script lang = "ts" >
import { ScrollArea } from "$lib/components/ui/scroll-area/index.js" ;
type Artwork = {
artist : string ;
art : string ;
};
const works : Artwork [] = [
{
artist: "Ornella Binni" ,
art: "https://images.unsplash.com/photo-1465869185982-5a1a7522cbcb?auto=format&fit=crop&w=300&q=80"
},
{
artist: "Tom Byrom" ,
art: "https://images.unsplash.com/photo-1548516173-3cabfa4607e9?auto=format&fit=crop&w=300&q=80"
},
{
artist: "Vladimir Malyavko" ,
art: "https://images.unsplash.com/photo-1494337480532-3725c85fd2ab?auto=format&fit=crop&w=300&q=80"
}
];
</ script >
< ScrollArea
class = "w-96 whitespace-nowrap rounded-md border"
orientation = "horizontal"
>
< div class = "flex w-max space-x-4 p-4" >
{# each works as artwork (artwork.artist)}
< figure class = "shrink-0" >
< div class = "overflow-hidden rounded-md" >
< img
src ={artwork.art}
alt ={ `Photo by ${ artwork . artist }` }
class = "aspect-[3/4] h-fit w-fit object-cover"
width ={ 300 }
height ={ 400 }
/>
</ div >
< figcaption class = "text-muted-foreground pt-2 text-xs" >
Photo by
< span class = "text-foreground font-semibold" >
{artwork.artist}
</ span >
</ figcaption >
</ figure >
{/ each }
</ div >
</ ScrollArea >
Copy
February 2024 New Component: Resizable We've added a new component to the project, Resizable , which is built on top of PaneForge . PaneForge is still in an early stage, so be sure to raise any issues you find with the library on the PaneForge GitHub .
< script lang = "ts" >
import * as Resizable from "$lib/components/ui/resizable/index.js" ;
</ script >
< Resizable . PaneGroup direction = "horizontal" class = "max-w-md rounded-lg border" >
< Resizable . Pane defaultSize ={ 50 }>
< div class = "flex h-[200px] items-center justify-center p-6" >
< span class = "font-semibold" >One</ span >
</ div >
</ Resizable . Pane >
< Resizable . Handle />
< Resizable . Pane defaultSize ={ 50 }>
< Resizable . PaneGroup direction = "vertical" >
< Resizable . Pane defaultSize ={ 25 }>
< div class = "flex h-full items-center justify-center p-6" >
< span class = "font-semibold" >Two</ span >
</ div >
</ Resizable . Pane >
< Resizable . Handle />
< Resizable . Pane defaultSize ={ 75 }>
< div class = "flex h-full items-center justify-center p-6" >
< span class = "font-semibold" >Three</ span >
</ div >
</ Resizable . Pane >
</ Resizable . PaneGroup >
</ Resizable . Pane >
</ Resizable . PaneGroup >
Copy
< script lang = "ts" >
import * as Resizable from "$lib/components/ui/resizable/index.js" ;
</ script >
< Resizable . PaneGroup direction = "horizontal" class = "max-w-md rounded-lg border" >
< Resizable . Pane defaultSize ={ 50 }>
< div class = "flex h-[200px] items-center justify-center p-6" >
< span class = "font-semibold" >One</ span >
</ div >
</ Resizable . Pane >
< Resizable . Handle />
< Resizable . Pane defaultSize ={ 50 }>
< Resizable . PaneGroup direction = "vertical" >
< Resizable . Pane defaultSize ={ 25 }>
< div class = "flex h-full items-center justify-center p-6" >
< span class = "font-semibold" >Two</ span >
</ div >
</ Resizable . Pane >
< Resizable . Handle />
< Resizable . Pane defaultSize ={ 75 }>
< div class = "flex h-full items-center justify-center p-6" >
< span class = "font-semibold" >Three</ span >
</ div >
</ Resizable . Pane >
</ Resizable . PaneGroup >
</ Resizable . Pane >
</ Resizable . PaneGroup >
Copy
Updated Icon Imports After some feedback about dev server performance, we've updated the way we import icons. With this change, we've decided to move away from the unmaintained radix-icons-svelte
package to svelte-radix for the new-york
style.
Instead of importing icons like so:
import { Check } from "lucide-svelte" ;
// or
import { Check } from "radix-icons-svelte" ;
Copy We now import them directly:
import Check from "lucide-svelte/icons/check" ;
// or
import Check from "svelte-radix/Check.svelte" ;
Copy With deep imports, we're preventing Vite from optimizing the entire icon collections, and instead only optimizing the icons that are actually used in your project. From what we've seen, this has a massive impact on dev server performance. Enjoy! 🚀
Formsnap has been completely rewritten to be more flexible, easier to use, and less opinionated. This means we've had to make some changes to the way we use it in shadcn-svelte
, but once you get the hang of it, you'll find it's much more powerful and less restrictive than the previous iteration.
Since the changes are so significant, there isn't a direct migration path from the old version to the new version. You'll need to update your components to use the new API, as well as ensure you're using the latest version of formsnap
and sveltekit-superforms
.
All of the Form
components have been updated to use the new API, and you can see live examples of them on the Forms Examples page.
Visit the Formsnap documentation (which has also been updated) to learn more about the new API and how its used.
January 2024 We've added four new components to the project, Carousel , Drawer , Sonner , & Pagination .
New Component: Carousel We've added a new component to the project, Carousel .
< script lang = "ts" >
import * as Card from "$lib/components/ui/card/index.js" ;
import * as Carousel from "$lib/components/ui/carousel/index.js" ;
</ script >
< Carousel . Root class = "w-full max-w-xs" >
< Carousel . Content >
{# each Array ( 5 ) as _, i (i)}
< Carousel . Item >
< div class = "p-1" >
< Card . Root >
< Card . Content
class = "flex aspect-square items-center justify-center p-6"
>
< span class = "text-4xl font-semibold" >{i + 1 }</ span >
</ Card . Content >
</ Card . Root >
</ div >
</ Carousel . Item >
{/ each }
</ Carousel . Content >
< Carousel . Previous />
< Carousel . Next />
</ Carousel . Root >
Copy
< script lang = "ts" >
import * as Card from "$lib/components/ui/card/index.js" ;
import * as Carousel from "$lib/components/ui/carousel/index.js" ;
</ script >
< Carousel . Root class = "w-full max-w-xs" >
< Carousel . Content >
{# each Array ( 5 ) as _, i (i)}
< Carousel . Item >
< div class = "p-1" >
< Card . Root >
< Card . Content
class = "flex aspect-square items-center justify-center p-6"
>
< span class = "text-4xl font-semibold" >{i + 1 }</ span >
</ Card . Content >
</ Card . Root >
</ div >
</ Carousel . Item >
{/ each }
</ Carousel . Content >
< Carousel . Previous />
< Carousel . Next />
</ Carousel . Root >
Copy
New Component: Drawer The Drawer is built on top of vaul-svelte and is a port of vaul , originally created by Emil Kowalski for React.
< script lang = "ts" >
import Minus from "lucide-svelte/icons/minus" ;
import Plus from "lucide-svelte/icons/plus" ;
import { VisGroupedBar, VisXYContainer } from "@unovis/svelte" ;
import {
Button,
buttonVariants
} from "$lib/components/ui/button/index.js" ;
import * as Drawer from "$lib/components/ui/drawer/index.js" ;
const data = [
{
id: 1 ,
goal: 400
},
{
id: 2 ,
goal: 300
},
{
id: 3 ,
goal: 200
},
{
id: 4 ,
goal: 300
},
{
id: 5 ,
goal: 200
},
{
id: 6 ,
goal: 278
},
{
id: 7 ,
goal: 189
},
{
id: 8 ,
goal: 239
},
{
id: 9 ,
goal: 300
},
{
id: 10 ,
goal: 200
},
{
id: 11 ,
goal: 278
},
{
id: 12 ,
goal: 189
},
{
id: 13 ,
goal: 349
}
];
const x = ( d : { goal : number ; id : number }) => d.id;
const y = ( d : { goal : number ; id : number }) => d.goal;
let goal = 350 ;
function handleClick ( adjustment : number ) {
goal = Math. max ( 200 , Math. min ( 400 , goal + adjustment));
}
</ script >
< Drawer . Root >
< Drawer . Trigger class ={ buttonVariants ({ variant: "outline" })}
>Open Drawer</ Drawer . Trigger
>
< Drawer . Content >
< div class = "mx-auto w-full max-w-sm" >
< Drawer . Header >
< Drawer . Title >Move Goal</ Drawer . Title >
< Drawer . Description >Set your daily activity goal.</ Drawer . Description >
</ Drawer . Header >
< div class = "p-4 pb-0" >
< div class = "flex items-center justify-center space-x-2" >
< Button
variant = "outline"
size = "icon"
class = "size-8 shrink-0 rounded-full"
onclick ={() => handleClick ( - 10 )}
disabled ={goal <= 200 }
>
< Minus />
< span class = "sr-only" >Decrease</ span >
</ Button >
< div class = "flex-1 text-center" >
< div class = "text-7xl font-bold tracking-tighter" >
{goal}
</ div >
< div class = "text-muted-foreground text-[0.70rem] uppercase" >
Calories/day
</ div >
</ div >
< Button
variant = "outline"
size = "icon"
class = "size-8 shrink-0 rounded-full"
onclick ={() => handleClick ( 10 )}
>
< Plus />
< span class = "sr-only" >Increase</ span >
</ Button >
</ div >
< div class = "mt-3 h-[120px]" >
< VisXYContainer { data } height ={ 60 }>
< VisGroupedBar { x } { y } color = "hsl(var(--primary) / 0.2)" />
</ VisXYContainer >
</ div >
</ div >
< Drawer . Footer >
< Button >Submit</ Button >
< Drawer . Close class ={ buttonVariants ({ variant: "outline" })}
>Cancel</ Drawer . Close
>
</ Drawer . Footer >
</ div >
</ Drawer . Content >
</ Drawer . Root >
Copy
< script lang = "ts" >
import Minus from "lucide-svelte/icons/minus" ;
import Plus from "lucide-svelte/icons/plus" ;
import { VisGroupedBar, VisXYContainer } from "@unovis/svelte" ;
import * as Drawer from "$lib/components/ui/drawer/index.js" ;
import {
Button,
buttonVariants
} from "$lib/components/ui/button/index.js" ;
const data = [
{
id: 1 ,
goal: 400
},
{
id: 2 ,
goal: 300
},
{
id: 3 ,
goal: 200
},
{
id: 4 ,
goal: 300
},
{
id: 5 ,
goal: 200
},
{
id: 6 ,
goal: 278
},
{
id: 7 ,
goal: 189
},
{
id: 8 ,
goal: 239
},
{
id: 9 ,
goal: 300
},
{
id: 10 ,
goal: 200
},
{
id: 11 ,
goal: 278
},
{
id: 12 ,
goal: 189
},
{
id: 13 ,
goal: 349
}
];
const x = ( d : { goal : number ; id : number }) => d.id;
const y = ( d : { goal : number ; id : number }) => d.goal;
let goal = $ state ( 350 );
function handleClick ( adjustment : number ) {
goal = Math. max ( 200 , Math. min ( 400 , goal + adjustment));
}
</ script >
< Drawer . Root >
< Drawer . Trigger class ={ buttonVariants ({ variant: "outline" })}
>Open Drawer</ Drawer . Trigger
>
< Drawer . Content >
< div class = "mx-auto w-full max-w-sm" >
< Drawer . Header >
< Drawer . Title >Move Goal</ Drawer . Title >
< Drawer . Description >Set your daily activity goal.</ Drawer . Description >
</ Drawer . Header >
< div class = "p-4 pb-0" >
< div class = "flex items-center justify-center space-x-2" >
< Button
variant = "outline"
size = "icon"
class = "size-8 shrink-0 rounded-full"
onclick ={() => handleClick ( - 10 )}
disabled ={goal <= 200 }
>
< Minus />
< span class = "sr-only" >Decrease</ span >
</ Button >
< div class = "flex-1 text-center" >
< div class = "text-7xl font-bold tracking-tighter" >
{goal}
</ div >
< div class = "text-muted-foreground text-[0.70rem] uppercase" >
Calories/day
</ div >
</ div >
< Button
variant = "outline"
size = "icon"
class = "size-8 shrink-0 rounded-full"
onclick ={() => handleClick ( 10 )}
disabled ={goal >= 400 }
>
< Plus />
< span class = "sr-only" >Increase</ span >
</ Button >
</ div >
< div class = "mt-3 h-[120px]" >
< VisXYContainer { data } height ={ 60 }>
< VisGroupedBar { x } { y } color = "hsl(var(--primary) / 0.2)" />
</ VisXYContainer >
</ div >
</ div >
< Drawer . Footer >
< Button >Submit</ Button >
< Drawer . Close class ={ buttonVariants ({ variant: "outline" })}
>Cancel</ Drawer . Close
>
</ Drawer . Footer >
</ div >
</ Drawer . Content >
</ Drawer . Root >
Copy
New Component: Sonner The Sonner component is provided by svelte-sonner , which is a Svelte port of Sonner , originally created by Emil Kowalski for React.
< script lang = "ts" >
import { toast } from "svelte-sonner" ;
import { Button } from "$lib/components/ui/button/index.js" ;
</ script >
< Button
variant = "outline"
onclick ={() =>
toast. success ( "Event has been created" , {
description: "Sunday, December 03, 2023 at 9:00 AM" ,
action: {
label: "Undo" ,
onClick : () => console. info ( "Undo" )
}
})}
>
Show Toast
</ Button >
Copy
< script lang = "ts" >
import { toast } from "svelte-sonner" ;
import { Button } from "$lib/components/ui/button/index.js" ;
</ script >
< Button
variant = "outline"
onclick ={() =>
toast. success ( "Event has been created" , {
description: "Sunday, December 03, 2023 at 9:00 AM" ,
action: {
label: "Undo" ,
onClick : () => console. info ( "Undo" )
}
})}
>
Show Toast
</ Button >
Copy
Pagination leverages the Pagination component from Bits UI.
< script lang = "ts" >
import { MediaQuery } from "svelte/reactivity" ;
import * as Pagination from "$lib/components/ui/pagination/index.js" ;
const isDesktop = new MediaQuery ( "(min-width: 768px)" );
const count = 20 ;
const perPage = $ derived (isDesktop.current ? 3 : 8 );
const siblingCount = $ derived (isDesktop.current ? 1 : 0 );
</ script >
< Pagination . Root { count } { perPage } { siblingCount } >
{# snippet children ({ pages, currentPage })}
< Pagination . Content >
< Pagination . Item >
< Pagination . PrevButton />
</ Pagination . Item >
{# each pages as page (page.key)}
{# if page.type === "ellipsis" }
< Pagination . Item >
< Pagination . Ellipsis />
</ Pagination . Item >
{: else }
< Pagination . Item >
< Pagination . Link { page } isActive ={currentPage === page.value}>
{page.value}
</ Pagination . Link >
</ Pagination . Item >
{/ if }
{/ each }
< Pagination . Item >
< Pagination . NextButton />
</ Pagination . Item >
</ Pagination . Content >
{/ snippet }
</ Pagination . Root >
Copy
< script lang = "ts" >
import ChevronLeft from "lucide-svelte/icons/chevron-left" ;
import ChevronRight from "lucide-svelte/icons/chevron-right" ;
import { MediaQuery } from "svelte/reactivity" ;
import * as Pagination from "$lib/components/ui/pagination/index.js" ;
const isDesktop = new MediaQuery ( "(min-width: 768px)" );
const count = 20 ;
const perPage = $ derived (isDesktop.current ? 3 : 8 );
const siblingCount = $ derived (isDesktop.current ? 1 : 0 );
</ script >
< Pagination . Root { count } { perPage } { siblingCount } >
{# snippet children ({ pages, currentPage })}
< Pagination . Content >
< Pagination . Item >
< Pagination . PrevButton >
< ChevronLeft class = "size-4" />
< span class = "hidden sm:block" >Previous</ span >
</ Pagination . PrevButton >
</ Pagination . Item >
{# each pages as page (page.key)}
{# if page.type === "ellipsis" }
< Pagination . Item >
< Pagination . Ellipsis />
</ Pagination . Item >
{: else }
< Pagination . Item >
< Pagination . Link { page } isActive ={currentPage === page.value}>
{page.value}
</ Pagination . Link >
</ Pagination . Item >
{/ if }
{/ each }
< Pagination . Item >
< Pagination . NextButton >
< span class = "hidden sm:block" >Next</ span >
< ChevronRight class = "size-4" />
</ Pagination . NextButton >
</ Pagination . Item >
</ Pagination . Content >
{/ snippet }
</ Pagination . Root >
Copy
December 2023 We've added three new components to the project, Calendar , Range Calendar , & Date Picker .
New Component: Calendar
< script lang = "ts" >
import { getLocalTimeZone, today } from "@internationalized/date" ;
import { Calendar } from "$lib/components/ui/calendar/index.js" ;
let value = $ state ( today ( getLocalTimeZone ()));
</ script >
< Calendar type = "single" bind : value class = "rounded-md border shadow" />
Copy
< script lang = "ts" >
import { getLocalTimeZone, today } from "@internationalized/date" ;
import { Calendar } from "$lib/components/ui/calendar/index.js" ;
let value = today ( getLocalTimeZone ());
</ script >
< Calendar type = "single" bind : value class = "rounded-md border" />
Copy
New Component: Range Calendar
< script lang = "ts" >
import { getLocalTimeZone, today } from "@internationalized/date" ;
import { RangeCalendar } from "$lib/components/ui/range-calendar/index.js" ;
const start = today ( getLocalTimeZone ());
const end = start. add ({ days: 7 });
let value = $ state ({
start,
end
});
</ script >
< RangeCalendar bind : value class = "rounded-md border shadow" />
Copy
< script lang = "ts" >
import { getLocalTimeZone, today } from "@internationalized/date" ;
import { RangeCalendar } from "$lib/components/ui/range-calendar/index.js" ;
const start = today ( getLocalTimeZone ());
const end = start. add ({ days: 7 });
let value = $ state ({
start,
end
});
</ script >
< RangeCalendar bind : value class = "rounded-md border" />
Copy
New Component: Date Picker
< script lang = "ts" >
import CalendarIcon from "lucide-svelte/icons/calendar" ;
import {
DateFormatter,
type DateValue,
getLocalTimeZone
} from "@internationalized/date" ;
import { cn } from "$lib/utils.js" ;
import { Button } from "$lib/components/ui/button/index.js" ;
import { Calendar } from "$lib/components/ui/calendar/index.js" ;
import * as Popover from "$lib/components/ui/popover/index.js" ;
const df = new DateFormatter ( "en-US" , {
dateStyle: "long"
});
let value : DateValue | undefined = undefined ;
</ script >
< Popover . Root >
< Popover . Trigger >
{# snippet child ({ props })}
< Button
variant = "outline"
class ={ cn (
"w-[240px] justify-start text-left font-normal" ,
! value && "text-muted-foreground"
)}
{ ... props }
>
< CalendarIcon />
{value ? df. format (value. toDate ( getLocalTimeZone ())) : "Pick a date" }
</ Button >
{/ snippet }
</ Popover . Trigger >
< Popover . Content class = "w-auto p-0" align = "start" >
< Calendar type = "single" bind : value />
</ Popover . Content >
</ Popover . Root >
Copy
< script lang = "ts" >
import CalendarIcon from "lucide-svelte/icons/calendar" ;
import {
DateFormatter,
type DateValue,
getLocalTimeZone
} from "@internationalized/date" ;
import { cn } from "$lib/utils.js" ;
import { buttonVariants } from "$lib/components/ui/button/index.js" ;
import { Calendar } from "$lib/components/ui/calendar/index.js" ;
import * as Popover from "$lib/components/ui/popover/index.js" ;
const df = new DateFormatter ( "en-US" , {
dateStyle: "long"
});
let value = $ state < DateValue | undefined >();
let contentRef = $ state < HTMLElement | null >( null );
</ script >
< Popover . Root >
< Popover . Trigger
class ={ cn (
buttonVariants ({
variant: "outline" ,
class: "w-[280px] justify-start text-left font-normal"
}),
! value && "text-muted-foreground"
)}
>
< CalendarIcon />
{value ? df. format (value. toDate ( getLocalTimeZone ())) : "Pick a date" }
</ Popover . Trigger >
< Popover . Content bind : ref ={contentRef} class = "w-auto p-0" >
< Calendar type = "single" bind : value />
</ Popover . Content >
</ Popover . Root >
Copy
November 2023 New Component: Toggle Group We've added a new component to the library, Toggle Group .
< script lang = "ts" >
import Bold from "lucide-svelte/icons/bold" ;
import Italic from "lucide-svelte/icons/italic" ;
import Underline from "lucide-svelte/icons/underline" ;
import * as ToggleGroup from "$lib/components/ui/toggle-group/index.js" ;
</ script >
< ToggleGroup . Root type = "multiple" >
< ToggleGroup . Item value = "bold" aria-label = "Toggle bold" >
< Bold />
</ ToggleGroup . Item >
< ToggleGroup . Item value = "italic" aria-label = "Toggle italic" >
< Italic />
</ ToggleGroup . Item >
< ToggleGroup . Item value = "strikethrough" aria-label = "Toggle strikethrough" >
< Underline />
</ ToggleGroup . Item >
</ ToggleGroup . Root >
Copy
< script lang = "ts" >
import Bold from "lucide-svelte/icons/bold" ;
import Italic from "lucide-svelte/icons/italic" ;
import Underline from "lucide-svelte/icons/underline" ;
import * as ToggleGroup from "$lib/components/ui/toggle-group/index.js" ;
</ script >
< ToggleGroup . Root type = "multiple" >
< ToggleGroup . Item value = "bold" aria-label = "Toggle bold" >
< Bold class = "size-4" />
</ ToggleGroup . Item >
< ToggleGroup . Item value = "italic" aria-label = "Toggle italic" >
< Italic class = "size-4" />
</ ToggleGroup . Item >
< ToggleGroup . Item value = "strikethrough" aria-label = "Toggle strikethrough" >
< Underline class = "size-4" />
</ ToggleGroup . Item >
</ ToggleGroup . Root >
Copy
October 2023 We've added two new components to the library, Command & Combobox . We've also made some updates to the <Form.Label />
component that you'll want to be aware of.
New Component: Command Command is a component that allows you to create a command palette. It's built on top of cmdk-sv , which is a Svelte port of cmdk . The library is still in its infancy, but we're excited to see where it goes. If you notice any issues, please open an issue with the library.
< script lang = "ts" >
import Calculator from "lucide-svelte/icons/calculator" ;
import Calendar from "lucide-svelte/icons/calendar" ;
import CreditCard from "lucide-svelte/icons/credit-card" ;
import Settings from "lucide-svelte/icons/settings" ;
import Smile from "lucide-svelte/icons/smile" ;
import User from "lucide-svelte/icons/user" ;
import { onMount } from "svelte" ;
import * as Command from "$lib/components/ui/command/index.js" ;
let open = $ state ( false );
onMount (() => {
function handleKeydown ( e : KeyboardEvent ) {
if (e.key === "j" && (e.metaKey || e.ctrlKey)) {
e. preventDefault ();
open = ! open;
}
}
document. addEventListener ( "keydown" , handleKeydown);
return () => {
document. removeEventListener ( "keydown" , handleKeydown);
};
});
</ script >
< p class = "text-muted-foreground text-sm" >
Press
< kbd
class = "bg-muted text-muted-foreground pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border px-1.5 font-mono text-[10px] font-medium opacity-100"
>
< span class = "text-xs" >⌘</ span >J
</ kbd >
</ p >
< Command . Dialog bind : open >
< Command . Input placeholder = "Type a command or search..." />
< Command . List >
< Command . Empty >No results found.</ Command . Empty >
< Command . Group heading = "Suggestions" >
< Command . Item >
< Calendar />
< span >Calendar</ span >
</ Command . Item >
< Command . Item >
< Smile />
< span >Search Emoji</ span >
</ Command . Item >
< Command . Item >
< Calculator />
< span >Calculator</ span >
</ Command . Item >
</ Command . Group >
< Command . Separator />
< Command . Group heading = "Settings" >
< Command . Item >
< User />
< span >Profile</ span >
< Command . Shortcut >⌘P</ Command . Shortcut >
</ Command . Item >
< Command . Item >
< CreditCard />
< span >Billing</ span >
< Command . Shortcut >⌘B</ Command . Shortcut >
</ Command . Item >
< Command . Item >
< Settings />
< span >Settings</ span >
< Command . Shortcut >⌘S</ Command . Shortcut >
</ Command . Item >
</ Command . Group >
</ Command . List >
</ Command . Dialog >
Copy
< script lang = "ts" >
import Calculator from "lucide-svelte/icons/calculator" ;
import Calendar from "lucide-svelte/icons/calendar" ;
import CreditCard from "lucide-svelte/icons/credit-card" ;
import Settings from "lucide-svelte/icons/settings" ;
import Smile from "lucide-svelte/icons/smile" ;
import User from "lucide-svelte/icons/user" ;
import * as Command from "$lib/components/ui/command/index.js" ;
let open = $ state ( false );
function handleKeydown ( e : KeyboardEvent ) {
if (e.key === "j" && (e.metaKey || e.ctrlKey)) {
e. preventDefault ();
open = ! open;
}
}
</ script >
< svelte : document onkeydown ={handleKeydown} />
< p class = "text-muted-foreground text-sm" >
Press
< kbd
class = "bg-muted text-muted-foreground pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border px-1.5 font-mono text-[10px] font-medium opacity-100"
>
< span class = "text-xs" >⌘</ span >J
</ kbd >
</ p >
< Command . Dialog bind : open >
< Command . Input placeholder = "Type a command or search..." />
< Command . List >
< Command . Empty >No results found.</ Command . Empty >
< Command . Group heading = "Suggestions" >
< Command . Item >
< Calendar class = "mr-2 size-4" />
< span >Calendar</ span >
</ Command . Item >
< Command . Item >
< Smile class = "mr-2 size-4" />
< span >Search Emoji</ span >
</ Command . Item >
< Command . Item >
< Calculator class = "mr-2 size-4" />
< span >Calculator</ span >
</ Command . Item >
</ Command . Group >
< Command . Separator />
< Command . Group heading = "Settings" >
< Command . Item >
< User class = "mr-2 size-4" />
< span >Profile</ span >
< Command . Shortcut >⌘P</ Command . Shortcut >
</ Command . Item >
< Command . Item >
< CreditCard class = "mr-2 size-4" />
< span >Billing</ span >
< Command . Shortcut >⌘B</ Command . Shortcut >
</ Command . Item >
< Command . Item >
< Settings class = "mr-2 size-4" />
< span >Settings</ span >
< Command . Shortcut >⌘S</ Command . Shortcut >
</ Command . Item >
</ Command . Group >
</ Command . List >
</ Command . Dialog >
Copy
Be sure to check out the Command docs for more information.
New Component: Combobox Combobox is a combination of the <Command />
& <Popover />
components. It allows you to create a searchable dropdown menu.
< script lang = "ts" >
import Check from "lucide-svelte/icons/check" ;
import ChevronsUpDown from "lucide-svelte/icons/chevrons-up-down" ;
import { tick } from "svelte" ;
import * as Command from "$lib/components/ui/command/index.js" ;
import * as Popover from "$lib/components/ui/popover/index.js" ;
import { Button } from "$lib/components/ui/button/index.js" ;
import { cn } from "$lib/utils.js" ;
const frameworks = [
{
value: "sveltekit" ,
label: "SvelteKit"
},
{
value: "next.js" ,
label: "Next.js"
},
{
value: "nuxt.js" ,
label: "Nuxt.js"
},
{
value: "remix" ,
label: "Remix"
},
{
value: "astro" ,
label: "Astro"
}
];
let open = $ state ( false );
let value = $ state ( "" );
let triggerRef = $ state < HTMLButtonElement >( null ! );
const selectedValue = $ derived (
frameworks. find (( f ) => f.value === value)?.label ?? "Select a framework..."
);
// We want to refocus the trigger button when the user selects
// an item from the list so users can continue navigating the
// rest of the form with the keyboard.
function closeAndFocusTrigger () {
open = false ;
tick (). then (() => {
triggerRef. focus ();
});
}
</ script >
< Popover . Root bind : open >
< Popover . Trigger bind : ref ={triggerRef}>
{# snippet child ({ props })}
< Button
variant = "outline"
class = "w-[200px] justify-between"
{ ... props }
role = "combobox"
aria-expanded ={open}
>
{selectedValue || "Select a framework..." }
< ChevronsUpDown class = "opacity-50" />
</ Button >
{/ snippet }
</ Popover . Trigger >
< Popover . Content class = "w-[200px] p-0" >
< Command . Root >
< Command . Input placeholder = "Search framework..." class = "h-9" />
< Command . List >
< Command . Empty >No framework found.</ Command . Empty >
< Command . Group >
{# each frameworks as framework}
< Command . Item
value ={framework.value}
onSelect ={() => {
value = framework.value;
closeAndFocusTrigger ();
}}
>
< Check
class ={ cn (value !== framework.value && "text-transparent" )}
/>
{framework.label}
</ Command . Item >
{/ each }
</ Command . Group >
</ Command . List >
</ Command . Root >
</ Popover . Content >
</ Popover . Root >
Copy
< script lang = "ts" >
import Check from "lucide-svelte/icons/check" ;
import ChevronsUpDown from "lucide-svelte/icons/chevrons-up-down" ;
import { tick } from "svelte" ;
import * as Command from "$lib/components/ui/command/index.js" ;
import * as Popover from "$lib/components/ui/popover/index.js" ;
import { Button } from "$lib/components/ui/button/index.js" ;
import { cn } from "$lib/utils.js" ;
const frameworks = [
{
value: "sveltekit" ,
label: "SvelteKit"
},
{
value: "next.js" ,
label: "Next.js"
},
{
value: "nuxt.js" ,
label: "Nuxt.js"
},
{
value: "remix" ,
label: "Remix"
},
{
value: "astro" ,
label: "Astro"
}
];
let open = $ state ( false );
let value = $ state ( "" );
let triggerRef = $ state < HTMLButtonElement >( null ! );
const selectedValue = $ derived (
frameworks. find (( f ) => f.value === value)?.label
);
// We want to refocus the trigger button when the user selects
// an item from the list so users can continue navigating the
// rest of the form with the keyboard.
function closeAndFocusTrigger () {
open = false ;
tick (). then (() => {
triggerRef. focus ();
});
}
</ script >
< Popover . Root bind : open >
< Popover . Trigger bind : ref ={triggerRef}>
{# snippet child ({ props })}
< Button
variant = "outline"
class = "w-[200px] justify-between"
{ ... props }
role = "combobox"
aria-expanded ={open}
>
{selectedValue || "Select a framework..." }
< ChevronsUpDown class = "opacity-50" />
</ Button >
{/ snippet }
</ Popover . Trigger >
< Popover . Content class = "w-[200px] p-0" >
< Command . Root >
< Command . Input placeholder = "Search framework..." />
< Command . List >
< Command . Empty >No framework found.</ Command . Empty >
< Command . Group >
{# each frameworks as framework}
< Command . Item
value ={framework.value}
onSelect ={() => {
value = framework.value;
closeAndFocusTrigger ();
}}
>
< Check
class ={ cn (value !== framework.value && "text-transparent" )}
/>
{framework.label}
</ Command . Item >
{/ each }
</ Command . Group >
</ Command . List >
</ Command . Root >
</ Popover . Content >
</ Popover . Root >
Copy
Be sure to check out the Combobox docs for more information.
Since we had to make some internal changes to formsnap to fix outstanding issues, there is a slight modification we have to make to the <Form.Label />
component. The ids
returned from getFormField()
is now a store, so we need to prefix it with $
when we use it.
form-label.svelte
< Label
for ={$ids.input}
class ={ cn ($errors && "text-destructive" , className)}
{ ... $$restProps }
>
< slot />
</ Label >
Copy Formsnap introduced a new component <Form.Control />
which wraps non-traditional form elements. This allows us to ensure the components are accessible, and work well with the rest of the form components. You'll need to define & export that control in your form/index.ts
file.
src/lib/ui/form/index.ts
// ...rest
const Control = FormPrimitive.Control;
export {
// ...rest
Control,
Control as FormControl,
};
Copy August 2023 - Transitions & More Transitions To support both enter and exit transitions, we've had to move from tailwindcss-animate
to Svelte transitions . You can still use the tailwindcss-animate
if you'd like, but you won't have exit transitions on most components.
To get the updated transition support, be sure to upgrade to the latest version of bits-ui
, which at the time of this writing is 0.5.0
.
We now provide a custom transition flyAndScale
(thanks @thomasglopes ) which most components use. It's added to the utils.ts
file when you init
a new project.
Migration If you're using tailwindcss-animate
and want to migrate to the new transition system, you'll need to do the following:
Update your utils.ts
file to include the flyAndScale
transition:
src/lib/utils.ts
import { type ClassValue, clsx } from "clsx" ;
import { twMerge } from "tailwind-merge" ;
import { cubicOut } from "svelte/easing" ;
import type { TransitionConfig } from "svelte/transition" ;
export function cn ( ... inputs : ClassValue []) {
return twMerge ( clsx (inputs));
}
type FlyAndScaleParams = {
y ?: number ;
x ?: number ;
start ?: number ;
duration ?: number ;
};
export const flyAndScale = (
node : Element ,
params : FlyAndScaleParams = { y: - 8 , x: 0 , start: 0.95 , duration: 150 }
) : TransitionConfig => {
const style = getComputedStyle (node);
const transform = style.transform === "none" ? "" : style.transform;
const scaleConversion = (
valueA : number ,
scaleA : [ number , number ],
scaleB : [ number , number ]
) => {
const [ minA , maxA ] = scaleA;
const [ minB , maxB ] = scaleB;
const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB;
return valueB;
};
const styleToString = (
style : Record < string , number | string | undefined >
) : string => {
return Object. keys (style). reduce (( str , key ) => {
if (style[key] === undefined ) return str;
return str + key + ":" + style[key] + ";" ;
}, "" );
};
return {
duration: params.duration ?? 200 ,
delay: 0 ,
css : ( t ) => {
const y = scaleConversion (t, [ 0 , 1 ], [params.y ?? 5 , 0 ]);
const x = scaleConversion (t, [ 0 , 1 ], [params.x ?? 0 , 0 ]);
const scale = scaleConversion (t, [ 0 , 1 ], [params.start ?? 0.95 , 1 ]);
return styleToString ({
transform:
transform +
"translate3d(" +
x +
"px, " +
y +
"px, 0) scale(" +
scale +
")" ,
opacity: t,
});
},
easing: cubicOut,
};
};
Copy Inside the components that use transitions/animations, you'll need to remove the animation classes and add the transition. Here's an example of the AlertDialog.Content
component:
src/lib/components/ui/alert-dialog-content.svelte
< script lang = "ts" >
import { AlertDialog as AlertDialogPrimitive } from "bits-ui" ;
import * as AlertDialog from "./index.js" ;
import { cn, flyAndScale } from "$lib/utils.js" ;
type $$Props = AlertDialogPrimitive . ContentProps ;
let className : $$Props [ "class" ] = undefined ;
export let transition : $$Props [ "transition" ] = flyAndScale;
export let transitionConfig : $$Props [ "transitionConfig" ] = undefined ;
export { className as class };
</ script >
< AlertDialog . Portal >
< AlertDialog . Overlay />
< AlertDialogPrimitive . Content
{ transition }
{ transitionConfig }
class ={ cn (
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg sm:rounded-lg md:w-full" ,
className
)}
{ ... $$restProps }
>
< slot />
</ AlertDialogPrimitive . Content >
</ AlertDialog . Portal >
Copy If you're unsure which specific classes should be removed, you can reference the components in the repo to see the changes.
Events Previous, we were using the same syntax as Melt UI for events, as we were simply forwarding them. So you'd have to do on:m-click
or on:m-keydown
. While this isn't a huge deal, since we're using components, we decided we wanted to use the same syntax as you would for any other Svelte component. So now you can just do on:click
or on:keydown
.
Behind the scenes, we're redispatching the event, so the contents of the event are the same, but the syntax is a bit more familiar.
Migration To migrate to the new event syntax, you'll need to update your components that are forwarding the m-
events. Ensure you're on the latest version of bits-ui
before doing so.