Changelog
Latest updates and announcements.
January 2024 - Part 2
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>
<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>
January 2024
We've added three new components to the project, Drawer, Sonner, & Pagination.
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 { Button } from "$lib/components/ui/button";
import * as Drawer from "$lib/components/ui/drawer";
import { Minus, Plus } from "radix-icons-svelte";
import { VisXYContainer, VisGroupedBar } from "@unovis/svelte";
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 asChild let:builder>
<Button builders={[builder]} variant="outline">Open Drawer</Button>
</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="h-8 w-8 shrink-0 rounded-full"
on:click={() => handleClick(-10)}
disabled={goal <= 200}
>
<Minus class="h-4 w-4" />
<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-[0.70rem] uppercase text-muted-foreground">
Calories/day
</div>
</div>
<Button
variant="outline"
size="icon"
class="h-8 w-8 shrink-0 rounded-full"
on:click={() => handleClick(10)}
>
<Plus class="h-4 w-4" />
<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 asChild let:builder>
<Button builders={[builder]} variant="outline">Cancel</Button>
</Drawer.Close>
</Drawer.Footer>
</div>
</Drawer.Content>
</Drawer.Root>
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as Drawer from "$lib/components/ui/drawer";
import { Minus, Plus } from "lucide-svelte";
import { VisXYContainer, VisGroupedBar } from "@unovis/svelte";
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 asChild let:builder>
<Button builders={[builder]} variant="outline">Open Drawer</Button>
</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="h-8 w-8 shrink-0 rounded-full"
on:click={() => handleClick(-10)}
disabled={goal <= 200}
>
<Minus class="h-4 w-4" />
<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-[0.70rem] uppercase text-muted-foreground">
Calories/day
</div>
</div>
<Button
variant="outline"
size="icon"
class="h-8 w-8 shrink-0 rounded-full"
on:click={() => handleClick(10)}
disabled={goal >= 400}
>
<Plus class="h-4 w-4" />
<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 asChild let:builder>
<Button builders={[builder]} variant="outline">Cancel</Button>
</Drawer.Close>
</Drawer.Footer>
</div>
</Drawer.Content>
</Drawer.Root>
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";
</script>
<Button
variant="outline"
on:click={() =>
toast("Event has been created", {
description: "Sunday, December 03, 2023 at 9:00 AM",
action: {
label: "Undo",
onClick: () => console.log("Undo")
}
})}
>
Show Toast
</Button>
<script lang="ts">
import { toast } from "svelte-sonner";
import { Button } from "$lib/components/ui/button";
</script>
<Button
variant="outline"
on:click={() =>
toast.success("Event has been created", {
description: "Sunday, December 03, 2023 at 9:00 AM",
action: {
label: "Undo",
onClick: () => console.log("Undo")
}
})}
>
Show Toast
</Button>
Pagination
Pagination leverages the Pagination component from Bits UI.
<script lang="ts">
import * as Pagination from "$lib/components/ui/pagination";
import { ChevronLeft, ChevronRight } from "radix-icons-svelte";
import { mediaQuery } from "svelte-legos";
const isDesktop = mediaQuery("(min-width: 768px)");
let count = 20;
$: perPage = $isDesktop ? 3 : 8;
$: siblingCount = $isDesktop ? 1 : 0;
</script>
<Pagination.Root {count} {perPage} {siblingCount} let:pages let:currentPage>
<Pagination.Content>
<Pagination.Item>
<Pagination.PrevButton>
<ChevronLeft class="h-4 w-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="h-4 w-4" />
</Pagination.NextButton>
</Pagination.Item>
</Pagination.Content>
</Pagination.Root>
<script lang="ts">
import * as Pagination from "$lib/components/ui/pagination";
import { ChevronLeft, ChevronRight } from "lucide-svelte";
import { mediaQuery } from "svelte-legos";
const isDesktop = mediaQuery("(min-width: 768px)");
let count = 20;
$: perPage = $isDesktop ? 3 : 8;
$: siblingCount = $isDesktop ? 1 : 0;
</script>
<Pagination.Root {count} {perPage} {siblingCount} let:pages let:currentPage>
<Pagination.Content>
<Pagination.Item>
<Pagination.PrevButton>
<ChevronLeft class="h-4 w-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="h-4 w-4" />
</Pagination.NextButton>
</Pagination.Item>
</Pagination.Content>
</Pagination.Root>
December 2023
We've added three new components to the project, Calendar, Range Calendar, & Date Picker.
Calendar
<script lang="ts">
import { Calendar } from "$lib/components/ui/calendar";
import { today, getLocalTimeZone } from "@internationalized/date";
let value = today(getLocalTimeZone());
</script>
<Calendar bind:value class="border rounded-md shadow" />
<script lang="ts">
import { Calendar } from "$lib/components/ui/calendar";
import { today, getLocalTimeZone } from "@internationalized/date";
let value = today(getLocalTimeZone());
</script>
<Calendar bind:value class="border rounded-md" />
Range Calendar
<script lang="ts">
import { RangeCalendar } from "$lib/components/ui/range-calendar";
import { today, getLocalTimeZone } from "@internationalized/date";
const start = today(getLocalTimeZone());
const end = start.add({ days: 7 });
let value = {
start,
end
};
</script>
<RangeCalendar bind:value class="border rounded-md shadow" />
<script lang="ts">
import { RangeCalendar } from "$lib/components/ui/range-calendar";
import { today, getLocalTimeZone } from "@internationalized/date";
const start = today(getLocalTimeZone());
const end = start.add({ days: 7 });
let value = {
start,
end
};
</script>
<RangeCalendar bind:value class="border rounded-md" />
Date Picker
<script lang="ts">
import { Calendar as CalendarIcon } from "radix-icons-svelte";
import {
type DateValue,
DateFormatter,
getLocalTimeZone
} from "@internationalized/date";
import { cn } from "$lib/utils";
import { Button } from "$lib/components/ui/button";
import { Calendar } from "$lib/components/ui/calendar";
import * as Popover from "$lib/components/ui/popover";
const df = new DateFormatter("en-US", {
dateStyle: "long"
});
let value: DateValue | undefined = undefined;
</script>
<Popover.Root>
<Popover.Trigger asChild let:builder>
<Button
variant="outline"
class={cn(
"w-[240px] justify-start text-left font-normal",
!value && "text-muted-foreground"
)}
builders={[builder]}
>
<CalendarIcon class="mr-2 h-4 w-4" />
{value ? df.format(value.toDate(getLocalTimeZone())) : "Pick a date"}
</Button>
</Popover.Trigger>
<Popover.Content class="w-auto p-0" align="start">
<Calendar bind:value />
</Popover.Content>
</Popover.Root>
<script lang="ts">
import { Calendar as CalendarIcon } from "lucide-svelte";
import {
type DateValue,
DateFormatter,
getLocalTimeZone
} from "@internationalized/date";
import { cn } from "$lib/utils";
import { Button } from "$lib/components/ui/button";
import { Calendar } from "$lib/components/ui/calendar";
import * as Popover from "$lib/components/ui/popover";
const df = new DateFormatter("en-US", {
dateStyle: "long"
});
let value: DateValue | undefined = undefined;
</script>
<Popover.Root>
<Popover.Trigger asChild let:builder>
<Button
variant="outline"
class={cn(
"w-[280px] justify-start text-left font-normal",
!value && "text-muted-foreground"
)}
builders={[builder]}
>
<CalendarIcon class="mr-2 h-4 w-4" />
{value ? df.format(value.toDate(getLocalTimeZone())) : "Pick a date"}
</Button>
</Popover.Trigger>
<Popover.Content class="w-auto p-0">
<Calendar bind:value initialFocus />
</Popover.Content>
</Popover.Root>
November 2023 - Toggle Group
We've added a new component to the library, Toggle Group.
<script lang="ts">
import { FontBold, FontItalic, Underline } from "radix-icons-svelte";
import * as ToggleGroup from "$lib/components/ui/toggle-group";
</script>
<ToggleGroup.Root type="multiple">
<ToggleGroup.Item value="bold" aria-label="Toggle bold">
<FontBold class="h-4 w-4" />
</ToggleGroup.Item>
<ToggleGroup.Item value="italic" aria-label="Toggle italic">
<FontItalic class="h-4 w-4" />
</ToggleGroup.Item>
<ToggleGroup.Item value="strikethrough" aria-label="Toggle strikethrough">
<Underline class="h-4 w-4" />
</ToggleGroup.Item>
</ToggleGroup.Root>
<script lang="ts">
import { Bold, Italic, Underline } from "lucide-svelte";
import * as ToggleGroup from "$lib/components/ui/toggle-group";
</script>
<ToggleGroup.Root type="multiple">
<ToggleGroup.Item value="bold" aria-label="Toggle bold">
<Bold class="h-4 w-4" />
</ToggleGroup.Item>
<ToggleGroup.Item value="italic" aria-label="Toggle italic">
<Italic class="h-4 w-4" />
</ToggleGroup.Item>
<ToggleGroup.Item value="strikethrough" aria-label="Toggle strikethrough">
<Underline class="h-4 w-4" />
</ToggleGroup.Item>
</ToggleGroup.Root>
October 2023 - New Components & Updates
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 {
Calendar,
EnvelopeClosed,
Face,
Gear,
Person,
Rocket
} from "radix-icons-svelte";
import * as Command from "$lib/components/ui/command";
import { onMount } from "svelte";
let open = 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-sm text-muted-foreground">
Press
<kbd
class="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground 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 h-4 w-4" />
<span>Calendar</span>
</Command.Item>
<Command.Item>
<Face class="mr-2 h-4 w-4" />
<span>Search Emoji</span>
</Command.Item>
<Command.Item>
<Rocket class="mr-2 h-4 w-4" />
<span>Launch</span>
</Command.Item>
</Command.Group>
<Command.Separator />
<Command.Group heading="Settings">
<Command.Item>
<Person class="mr-2 h-4 w-4" />
<span>Profile</span>
<Command.Shortcut>⌘P</Command.Shortcut>
</Command.Item>
<Command.Item>
<EnvelopeClosed class="mr-2 h-4 w-4" />
<span>Mail</span>
<Command.Shortcut>⌘B</Command.Shortcut>
</Command.Item>
<Command.Item>
<Gear class="mr-2 h-4 w-4" />
<span>Settings</span>
<Command.Shortcut>⌘S</Command.Shortcut>
</Command.Item>
</Command.Group>
</Command.List>
</Command.Dialog>
<script lang="ts">
import {
Calculator,
Calendar,
CreditCard,
Settings,
Smile,
User
} from "lucide-svelte";
import * as Command from "$lib/components/ui/command";
import { onMount } from "svelte";
let open = 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-sm text-muted-foreground">
Press
<kbd
class="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground 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 h-4 w-4" />
<span>Calendar</span>
</Command.Item>
<Command.Item>
<Smile class="mr-2 h-4 w-4" />
<span>Search Emoji</span>
</Command.Item>
<Command.Item>
<Calculator class="mr-2 h-4 w-4" />
<span>Calculator</span>
</Command.Item>
</Command.Group>
<Command.Separator />
<Command.Group heading="Settings">
<Command.Item>
<User class="mr-2 h-4 w-4" />
<span>Profile</span>
<Command.Shortcut>⌘P</Command.Shortcut>
</Command.Item>
<Command.Item>
<CreditCard class="mr-2 h-4 w-4" />
<span>Billing</span>
<Command.Shortcut>⌘B</Command.Shortcut>
</Command.Item>
<Command.Item>
<Settings class="mr-2 h-4 w-4" />
<span>Settings</span>
<Command.Shortcut>⌘S</Command.Shortcut>
</Command.Item>
</Command.Group>
</Command.List>
</Command.Dialog>
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, CaretSort } from "radix-icons-svelte";
import * as Command from "$lib/components/ui/command";
import * as Popover from "$lib/components/ui/popover";
import { Button } from "$lib/components/ui/button";
import { cn } from "$lib/utils";
import { tick } from "svelte";
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 = false;
let value = "";
$: selectedValue =
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(triggerId: string) {
open = false;
tick().then(() => {
document.getElementById(triggerId)?.focus();
});
}
</script>
<Popover.Root bind:open let:ids>
<Popover.Trigger asChild let:builder>
<Button
builders={[builder]}
variant="outline"
role="combobox"
aria-expanded={open}
class="w-[200px] justify-between"
>
{selectedValue}
<CaretSort class="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</Popover.Trigger>
<Popover.Content class="w-[200px] p-0">
<Command.Root>
<Command.Input placeholder="Search framework..." class="h-9" />
<Command.Empty>No framework found.</Command.Empty>
<Command.Group>
{#each frameworks as framework}
<Command.Item
value={framework.value}
onSelect={(currentValue) => {
value = currentValue;
closeAndFocusTrigger(ids.trigger);
}}
>
<Check
class={cn(
"mr-2 h-4 w-4",
value !== framework.value && "text-transparent"
)}
/>
{framework.label}
</Command.Item>
{/each}
</Command.Group>
</Command.Root>
</Popover.Content>
</Popover.Root>
<script lang="ts">
import { Check, ChevronsUpDown } from "lucide-svelte";
import * as Command from "$lib/components/ui/command";
import * as Popover from "$lib/components/ui/popover";
import { Button } from "$lib/components/ui/button";
import { cn } from "$lib/utils";
import { tick } from "svelte";
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 = false;
let value = "";
$: selectedValue =
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(triggerId: string) {
open = false;
tick().then(() => {
document.getElementById(triggerId)?.focus();
});
}
</script>
<Popover.Root bind:open let:ids>
<Popover.Trigger asChild let:builder>
<Button
builders={[builder]}
variant="outline"
role="combobox"
aria-expanded={open}
class="w-[200px] justify-between"
>
{selectedValue}
<ChevronsUpDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</Popover.Trigger>
<Popover.Content class="w-[200px] p-0">
<Command.Root>
<Command.Input placeholder="Search framework..." />
<Command.Empty>No framework found.</Command.Empty>
<Command.Group>
{#each frameworks as framework}
<Command.Item
value={framework.value}
onSelect={(currentValue) => {
value = currentValue;
closeAndFocusTrigger(ids.trigger);
}}
>
<Check
class={cn(
"mr-2 h-4 w-4",
value !== framework.value && "text-transparent"
)}
/>
{framework.label}
</Command.Item>
{/each}
</Command.Group>
</Command.Root>
</Popover.Content>
</Popover.Root>
Be sure to check out the Combobox docs for more information.
Updates to Form
Form.Label Changes
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.
<Label
for={$ids.input}
class={cn($errors && "text-destructive", className)}
{...$$restProps}
>
<slot />
</Label>
Form.Control
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.
// ...rest
const Control = FormPrimitive.Control;
export {
// ...rest
Control,
Control as FormControl,
};
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:
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,
};
};
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:
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import * as AlertDialog from ".";
import { cn, flyAndScale } from "$lib/utils";
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>
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.
On This Page