Popover
Display supplementary content or information when users interact with specific elements.
<script lang="ts">
import { Popover, Separator, Toggle } from "bits-ui";
import ImageSquare from "phosphor-svelte/lib/ImageSquare";
import LinkSimpleHorizontalBreak from "phosphor-svelte/lib/LinkSimpleHorizontalBreak";
let width = $state(1024);
let height = $state(768);
</script>
<Popover.Root>
<Popover.Trigger
class="inline-flex h-10
select-none items-center justify-center whitespace-nowrap rounded-input bg-dark px-[21px] text-[15px] font-medium text-background shadow-mini transition-all hover:cursor-pointer hover:bg-dark/95 active:scale-98"
>
Resize
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
class="z-30 w-full max-w-[328px] rounded-[12px] border border-dark-10 bg-background p-4 shadow-popover data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
sideOffset={8}
>
<div class="flex items-center">
<div
class="mr-3 flex size-12 items-center justify-center rounded-full bg-muted"
>
<ImageSquare class="size-6" />
</div>
<div class="flex flex-col">
<h4 class="text-[17px] font-semibold leading-5 tracking-[-0.01em]">
Resize image
</h4>
<p class="text-sm font-medium text-muted-foreground">
Resize your photos easily
</p>
</div>
</div>
<Separator.Root class="-mx-4 mb-6 mt-[17px] block h-px bg-dark-10" />
<div class="flex items-center pb-2">
<div class="mr-2 flex items-center">
<div class="relative mr-2">
<span class="sr-only">Width</span>
<span
aria-hidden="true"
class="absolute left-5 top-4 text-xxs text-muted-foreground"
>W</span
>
<input
type="number"
class="h-input w-[119px] rounded-10px border border-border-input bg-background pl-10 pr-2 text-sm text-foreground"
bind:value={width}
/>
</div>
<div class="relative">
<span class="sr-only">Height</span>
<span
aria-hidden="true"
class="absolute left-5 top-4 text-xxs text-muted-foreground"
>H</span
>
<input
type="number"
class="h-input w-[119px] rounded-10px border border-border-input bg-background pl-10 pr-2 text-sm text-foreground"
bind:value={height}
/>
</div>
</div>
<Toggle.Root
aria-label="toggle constrain portions"
class="inline-flex size-10 items-center justify-center rounded-[9px] bg-background transition-all hover:bg-muted active:scale-98 data-[state=on]:bg-muted"
>
<LinkSimpleHorizontalBreak class="size-6" />
</Toggle.Root>
</div>
</Popover.Content>
</Popover.Portal>
</Popover.Root>
import typography from "@tailwindcss/typography";
import animate from "tailwindcss-animate";
import { fontFamily } from "tailwindcss/defaultTheme";
/** @type {import('tailwindcss').Config} */
export default {
darkMode: "class",
content: ["./src/**/*.{html,js,svelte,ts}"],
theme: {
container: {
center: true,
screens: {
"2xl": "1440px",
},
},
extend: {
colors: {
border: {
DEFAULT: "hsl(var(--border-card))",
input: "hsl(var(--border-input))",
"input-hover": "hsl(var(--border-input-hover))",
},
background: {
DEFAULT: "hsl(var(--background) / <alpha-value>)",
alt: "hsl(var(--background-alt) / <alpha-value>)",
},
foreground: {
DEFAULT: "hsl(var(--foreground) / <alpha-value>)",
alt: "hsl(var(--foreground-alt) / <alpha-value>)",
},
muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
foreground: "hsl(var(--muted-foreground))",
},
dark: {
DEFAULT: "hsl(var(--dark) / <alpha-value>)",
4: "hsl(var(--dark-04))",
10: "hsl(var(--dark-10))",
40: "hsl(var(--dark-40))",
},
accent: {
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
foreground: "hsl(var(--accent-foreground) / <alpha-value>)",
},
destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
},
contrast: {
DEFAULT: "hsl(var(--contrast) / <alpha-value>)",
},
},
fontFamily: {
sans: ["Inter", ...fontFamily.sans],
mono: ["Source Code Pro", ...fontFamily.mono],
alt: ["Courier", ...fontFamily.sans],
},
fontSize: {
xxs: "10px",
},
borderWidth: {
6: "6px",
},
borderRadius: {
card: "16px",
"card-lg": "20px",
"card-sm": "10px",
input: "9px",
button: "5px",
"5px": "5px",
"9px": "9px",
"10px": "10px",
"15px": "15px",
},
height: {
input: "3rem",
"input-sm": "2.5rem",
},
boxShadow: {
mini: "var(--shadow-mini)",
"mini-inset": "var(--shadow-mini-inset)",
popover: "var(--shadow-popover)",
kbd: "var(--shadow-kbd)",
btn: "var(--shadow-btn)",
card: "var(--shadow-card)",
"date-field-focus": "var(--shadow-date-field-focus)",
},
opacity: {
8: "0.08",
},
scale: {
80: ".80",
98: ".98",
99: ".99",
},
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--bits-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--bits-accordion-content-height)" },
to: { height: "0" },
},
"caret-blink": {
"0%,70%,100%": { opacity: "1" },
"20%,50%": { opacity: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"caret-blink": "caret-blink 1.25s ease-out infinite",
},
},
plugins: [typography, animate],
};
@import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* Colors */
--background: 0 0% 100%;
--background-alt: 0 0% 100%;
--foreground: 0 0% 9%;
--foreground-alt: 0 0% 32%;
--muted: 240 5% 96%;
--muted-foreground: 0 0% 9% / 0.4;
--border: 240 6% 10%;
--border-input: 240 6% 10% / 0.17;
--border-input-hover: 240 6% 10% / 0.4;
--border-card: 240 6% 10% / 0.1;
--dark: 240 6% 10%;
--dark-10: 240 6% 10% / 0.1;
--dark-40: 240 6% 10% / 0.4;
--dark-04: 240 6% 10% / 0.04;
--accent: 204 94% 94%;
--accent-foreground: 204 80% 16%;
--destructive: 347 77% 50%;
/* black */
--constrast: 0 0% 0%;
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.04) inset;
--shadow-popover: 0px 7px 12px 3px hsla(var(--dark-10));
--shadow-kbd: 0px 2px 0px 0px rgba(0, 0, 0, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.03);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(24, 24, 27, 0.17);
}
.dark {
/* Colors */
--background: 0 0% 5%;
--background-alt: 0 0% 8%;
--foreground: 0 0% 95%;
--foreground-alt: 0 0% 70%;
--muted: 240 4% 16%;
--muted-foreground: 0 0% 100% / 0.4;
--border: 0 0% 96%;
--border-input: 0 0% 96% / 0.17;
--border-input-hover: 0 0% 96% / 0.4;
--border-card: 0 0% 96% / 0.1;
--dark: 0 0% 96%;
--dark-40: 0 0% 96% / 0.4;
--dark-10: 0 0% 96% / 0.1;
--dark-04: 0 0% 96% / 0.04;
--accent: 204 90 90%;
--accent-foreground: 204 94% 94%;
--destructive: 350 89% 60%;
/* white */
--constrast: 0 0% 100%;
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.3);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.5) inset;
--shadow-popover: 0px 7px 12px 3px hsla(0deg 0% 0% / 30%);
--shadow-kbd: 0px 2px 0px 0px rgba(255, 255, 255, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.2);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.4);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(244, 244, 245, 0.1);
}
}
@layer base {
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
/* Mobile tap highlight */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-tap-highlight-color */
html {
-webkit-tap-highlight-color: rgba(128, 128, 128, 0.5);
}
::selection {
background: #fdffa4;
color: black;
}
/* === Scrollbars === */
::-webkit-scrollbar {
@apply w-2;
@apply h-2;
}
::-webkit-scrollbar-track {
@apply !bg-transparent;
}
::-webkit-scrollbar-thumb {
@apply rounded-card-lg !bg-dark-10;
}
::-webkit-scrollbar-corner {
background: rgba(0, 0, 0, 0);
}
/* Firefox */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color#browser_compatibility */
html {
scrollbar-color: var(--bg-muted);
}
.antialised {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
@layer utilities {
.step {
counter-increment: step;
}
.step:before {
@apply absolute inline-flex h-9 w-9 items-center justify-center rounded-full border-4 border-background bg-muted text-center -indent-px font-mono text-base font-medium;
@apply ml-[-50px] mt-[-4px];
content: counter(step);
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
}
.link {
@apply inline-flex items-center gap-1 rounded-sm font-medium underline underline-offset-4 hover:text-foreground/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type="number"] {
-moz-appearance: textfield;
}
}
Structure
<script lang="ts">
import { Popover } from "bits-ui";
</script>
<Popover.Root>
<Popover.Trigger />
<Popover.Content>
<Popover.Close />
<Popover.Arrow />
</Popover.Content>
</Popover.Root>
Managing Open State
Bits UI offers several approaches to manage and synchronize the Popover's open state, catering to different levels of control and integration needs.
1. Two-Way Binding
For seamless state synchronization, use Svelte's bind:open
directive. This method automatically keeps your local state in sync with the dialog's internal state.
<script lang="ts">
import { Popover } from "bits-ui";
let isOpen = $state(false);
</script>
<button onclick={() => (isOpen = true)}>Open Popover</button>
<Popover.Root bind:open={isOpen}>
<!-- ... -->
</Popover.Root>
Key Benefits
- Simplifies state management
- Automatically updates
isOpen
when the popover closes/opens (e.g., via escape key) - Allows external control (e.g., opening via a separate button)
2. Change Handler
For more granular control or to perform additional logic on state changes, use the onOpenChange
prop. This approach is useful when you need to execute custom logic alongside state updates.
<script lang="ts">
import { Popover } from "bits-ui";
let isOpen = $state(false);
</script>
<Popover.Root
open={isOpen}
onOpenChange={(o) => {
isOpen = o;
// additional logic here.
}}
>
<!-- ... -->
</Popover.Root>
Use Cases
- Implementing custom behaviors on open/close
- Integrating with external state management solutions
- Triggering side effects (e.g., logging, data fetching)
3. Fully Controlled
For complete control over the component's state, use a Function Binding to manage the value state externally.
<script lang="ts">
import { Popover } from "bits-ui";
let myOpen = $state(false);
</script>
<Popover.Root bind:open={() => myOpen, (newOpen) => (myOpen = newOpen)}>
<!-- ... -->
</Popover.Root>
When to Use
- Implementing complex open/close logic
- Coordinating multiple UI elements
- Debugging state-related issues
Note
While powerful, fully controlled state should be used judiciously as it increases complexity and can cause unexpected behaviors if not handled carefully.
For more in-depth information on controlled components and advanced state management techniques, refer to our Controlled State documentation.
Managing Focus
Focus Trap
By default, when a Popover is opened, focus will be trapped within that Popover. You can disable this behavior by setting the trapFocus
prop to false
on the Popover.Content
component.
<Popover.Content trapFocus={false}>
<!-- ... -->
</Popover.Content>
Open Focus
By default, when a Popover is opened, focus will be set to the first focusable element with the Popover.Content
. This ensures that users navigating my keyboard end up somewhere within the Popover that they can interact with.
You can override this behavior using the onOpenAutoFocus
prop on the Popover.Content
component. It's highly recommended that you use this prop to focus something within the Popover's content.
You'll first need to cancel the default behavior of focusing the first focusable element by cancelling the event passed to the onOpenAutoFocus
callback. You can then focus whatever you wish.
<script lang="ts">
import { Popover } from "bits-ui";
let nameInput = $state<HTMLInputElement>();
</script>
<Popover.Root>
<Popover.Trigger>Open Popover</Popover.Trigger>
<Popover.Content
onOpenAutoFocus={(e) => {
e.preventDefault();
nameInput?.focus();
}}
>
<input type="text" bind:this={nameInput} />
</Popover.Content>
</Popover.Root>
Close Focus
By default, when a Popover is closed, focus will be set to the trigger element of the Popover. You can override this behavior using the onCloseAutoFocus
prop on the Popover.Content
component.
You'll need to cancel the default behavior of focusing the trigger element by cancelling the event passed to the onCloseAutoFocus
callback, and then focus whatever you wish.
<script lang="ts">
import { Popover } from "bits-ui";
let nameInput = $state<HTMLInputElement>();
</script>
<input type="text" bind:this={nameInput} />
<Popover.Root>
<Popover.Trigger>Open Popover</Popover.Trigger>
<Popover.Content
onCloseAutoFocus={(e) => {
e.preventDefault();
nameInput?.focus();
}}
>
<!-- ... -->
</Popover.Content>
</Popover.Root>
Scroll Lock
By default, when a Popover is opened, users can still scroll the body and interact with content outside of the Popover. If you wish to lock the body scroll and prevent users from interacting with content outside of the Popover, you can set the preventScroll
prop to true
on the Popover.Content
component.
<Popover.Content preventScroll={true}>
<!-- ... -->
</Popover.Content>
Escape Keydown
By default, when a Popover is open, pressing the Escape
key will close the dialog. Bits UI provides a couple ways to override this behavior.
escapeKeydownBehavior
You can set the escapeKeydownBehavior
prop to 'ignore'
on the Popover.Content
component to prevent the dialog from closing when the Escape
key is pressed.
<Popover.Content escapeKeydownBehavior="ignore">
<!-- ... -->
</Popover.Content>
onEscapeKeydown
You can also override the default behavior by cancelling the event passed to the onEscapeKeydown
callback on the Popover.Content
component.
<Popover.Content onEscapeKeydown={(e) => e.preventDefault()}>
<!-- ... -->
</Popover.Content>
Interact Outside
By default, when a Popover is open, pointer down events outside the content will close the popover. Bits UI provides a couple ways to override this behavior.
interactOutsideBehavior
You can set the interactOutsideBehavior
prop to 'ignore'
on the Popover.Content
component to prevent the dialog from closing when the user interacts outside the content.
<Popover.Content interactOutsideBehavior="ignore">
<!-- ... -->
</Popover.Content>
onInteractOutside
You can also override the default behavior by cancelling the event passed to the onInteractOutside
callback on the Popover.Content
component.
<Popover.Content onInteractOutside={(e) => e.preventDefault()}>
<!-- ... -->
</Popover.Content>
Custom Anchor
By default, the Popover.Content
is anchored to the Popover.Trigger
component, which determines where the content is positioned.
If you wish to instead anchor the content to a different element, you can pass either a selector string
or an HTMLElement
to the customAnchor
prop of the Popover.Content
component.
<script lang="ts">
import { Popover } from "bits-ui";
let customAnchor = $state<HTMLElement>(null!);
</script>
<div bind:this={customAnchor}></div>
<Popover.Root>
<Popover.Trigger />
<Popover.Content {customAnchor}>
<!-- ... -->
</Popover.Content>
</Popover.Root>
Svelte Transitions
You can use the forceMount
prop along with the child
snippet to forcefully mount the Popover.Content
component to use Svelte Transitions or another animation library that requires more control.
<script lang="ts">
import { Popover } from "bits-ui";
import { fly } from "svelte/transition";
</script>
<Popover.Content forceMount>
{#snippet child({ wrapperProps, props, open })}
{#if open}
<div {...wrapperProps}>
<div {...props} transition:fly>
<!-- ... -->
</div>
</div>
{/if}
{/snippet}
</Popover.Content>
Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the Transitions documentation.
<script lang="ts">
import { Popover, Separator, Toggle } from "bits-ui";
import ImageSquare from "phosphor-svelte/lib/ImageSquare";
import LinkSimpleHorizontalBreak from "phosphor-svelte/lib/LinkSimpleHorizontalBreak";
import { fly } from "svelte/transition";
let width = $state(1024);
let height = $state(768);
</script>
<Popover.Root>
<Popover.Trigger
class="inline-flex h-10
select-none items-center justify-center whitespace-nowrap rounded-input bg-dark px-[21px] text-[15px] font-medium text-background shadow-mini transition-all hover:cursor-pointer hover:bg-dark/95 active:scale-98"
>
Resize
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
class="z-30 w-full max-w-[328px] rounded-[12px] border border-dark-10 bg-background p-4 shadow-popover"
sideOffset={8}
forceMount
>
{#snippet child({ wrapperProps, props, open })}
{#if open}
<div {...wrapperProps}>
<div {...props} transition:fly={{ duration: 300 }}>
<div class="flex items-center">
<div
class="mr-3 flex size-12 items-center justify-center rounded-full bg-muted"
>
<ImageSquare class="size-6" />
</div>
<div class="flex flex-col">
<h4
class="text-[17px] font-semibold leading-5 tracking-[-0.01em]"
>
Resize image
</h4>
<p class="text-sm font-medium text-muted-foreground">
Resize your photos easily
</p>
</div>
</div>
<Separator.Root
class="-mx-4 mb-6 mt-[17px] block h-px bg-dark-10"
/>
<div class="flex items-center pb-2">
<div class="mr-2 flex items-center">
<div class="relative mr-2">
<span class="sr-only">Width</span>
<span
aria-hidden="true"
class="absolute left-5 top-4 text-xxs text-muted-foreground"
>W</span
>
<input
type="number"
class="h-input w-[119px] rounded-10px border border-border-input bg-background pl-10 pr-2 text-sm text-foreground"
bind:value={width}
/>
</div>
<div class="relative">
<span class="sr-only">Height</span>
<span
aria-hidden="true"
class="absolute left-5 top-4 text-xxs text-muted-foreground"
>H</span
>
<input
type="number"
class="h-input w-[119px] rounded-10px border border-border-input bg-background pl-10 pr-2 text-sm text-foreground"
bind:value={height}
/>
</div>
</div>
<Toggle.Root
aria-label="toggle constrain portions"
class="inline-flex size-10 items-center justify-center rounded-[9px] bg-background transition-all hover:bg-muted active:scale-98 data-[state=on]:bg-muted"
>
<LinkSimpleHorizontalBreak class="size-6" />
</Toggle.Root>
</div>
</div>
</div>
{/if}
{/snippet}
</Popover.Content>
</Popover.Portal>
</Popover.Root>
import typography from "@tailwindcss/typography";
import animate from "tailwindcss-animate";
import { fontFamily } from "tailwindcss/defaultTheme";
/** @type {import('tailwindcss').Config} */
export default {
darkMode: "class",
content: ["./src/**/*.{html,js,svelte,ts}"],
theme: {
container: {
center: true,
screens: {
"2xl": "1440px",
},
},
extend: {
colors: {
border: {
DEFAULT: "hsl(var(--border-card))",
input: "hsl(var(--border-input))",
"input-hover": "hsl(var(--border-input-hover))",
},
background: {
DEFAULT: "hsl(var(--background) / <alpha-value>)",
alt: "hsl(var(--background-alt) / <alpha-value>)",
},
foreground: {
DEFAULT: "hsl(var(--foreground) / <alpha-value>)",
alt: "hsl(var(--foreground-alt) / <alpha-value>)",
},
muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
foreground: "hsl(var(--muted-foreground))",
},
dark: {
DEFAULT: "hsl(var(--dark) / <alpha-value>)",
4: "hsl(var(--dark-04))",
10: "hsl(var(--dark-10))",
40: "hsl(var(--dark-40))",
},
accent: {
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
foreground: "hsl(var(--accent-foreground) / <alpha-value>)",
},
destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
},
contrast: {
DEFAULT: "hsl(var(--contrast) / <alpha-value>)",
},
},
fontFamily: {
sans: ["Inter", ...fontFamily.sans],
mono: ["Source Code Pro", ...fontFamily.mono],
alt: ["Courier", ...fontFamily.sans],
},
fontSize: {
xxs: "10px",
},
borderWidth: {
6: "6px",
},
borderRadius: {
card: "16px",
"card-lg": "20px",
"card-sm": "10px",
input: "9px",
button: "5px",
"5px": "5px",
"9px": "9px",
"10px": "10px",
"15px": "15px",
},
height: {
input: "3rem",
"input-sm": "2.5rem",
},
boxShadow: {
mini: "var(--shadow-mini)",
"mini-inset": "var(--shadow-mini-inset)",
popover: "var(--shadow-popover)",
kbd: "var(--shadow-kbd)",
btn: "var(--shadow-btn)",
card: "var(--shadow-card)",
"date-field-focus": "var(--shadow-date-field-focus)",
},
opacity: {
8: "0.08",
},
scale: {
80: ".80",
98: ".98",
99: ".99",
},
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--bits-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--bits-accordion-content-height)" },
to: { height: "0" },
},
"caret-blink": {
"0%,70%,100%": { opacity: "1" },
"20%,50%": { opacity: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"caret-blink": "caret-blink 1.25s ease-out infinite",
},
},
plugins: [typography, animate],
};
@import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* Colors */
--background: 0 0% 100%;
--background-alt: 0 0% 100%;
--foreground: 0 0% 9%;
--foreground-alt: 0 0% 32%;
--muted: 240 5% 96%;
--muted-foreground: 0 0% 9% / 0.4;
--border: 240 6% 10%;
--border-input: 240 6% 10% / 0.17;
--border-input-hover: 240 6% 10% / 0.4;
--border-card: 240 6% 10% / 0.1;
--dark: 240 6% 10%;
--dark-10: 240 6% 10% / 0.1;
--dark-40: 240 6% 10% / 0.4;
--dark-04: 240 6% 10% / 0.04;
--accent: 204 94% 94%;
--accent-foreground: 204 80% 16%;
--destructive: 347 77% 50%;
/* black */
--constrast: 0 0% 0%;
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.04) inset;
--shadow-popover: 0px 7px 12px 3px hsla(var(--dark-10));
--shadow-kbd: 0px 2px 0px 0px rgba(0, 0, 0, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.03);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(24, 24, 27, 0.17);
}
.dark {
/* Colors */
--background: 0 0% 5%;
--background-alt: 0 0% 8%;
--foreground: 0 0% 95%;
--foreground-alt: 0 0% 70%;
--muted: 240 4% 16%;
--muted-foreground: 0 0% 100% / 0.4;
--border: 0 0% 96%;
--border-input: 0 0% 96% / 0.17;
--border-input-hover: 0 0% 96% / 0.4;
--border-card: 0 0% 96% / 0.1;
--dark: 0 0% 96%;
--dark-40: 0 0% 96% / 0.4;
--dark-10: 0 0% 96% / 0.1;
--dark-04: 0 0% 96% / 0.04;
--accent: 204 90 90%;
--accent-foreground: 204 94% 94%;
--destructive: 350 89% 60%;
/* white */
--constrast: 0 0% 100%;
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.3);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.5) inset;
--shadow-popover: 0px 7px 12px 3px hsla(0deg 0% 0% / 30%);
--shadow-kbd: 0px 2px 0px 0px rgba(255, 255, 255, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.2);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.4);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(244, 244, 245, 0.1);
}
}
@layer base {
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
/* Mobile tap highlight */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-tap-highlight-color */
html {
-webkit-tap-highlight-color: rgba(128, 128, 128, 0.5);
}
::selection {
background: #fdffa4;
color: black;
}
/* === Scrollbars === */
::-webkit-scrollbar {
@apply w-2;
@apply h-2;
}
::-webkit-scrollbar-track {
@apply !bg-transparent;
}
::-webkit-scrollbar-thumb {
@apply rounded-card-lg !bg-dark-10;
}
::-webkit-scrollbar-corner {
background: rgba(0, 0, 0, 0);
}
/* Firefox */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color#browser_compatibility */
html {
scrollbar-color: var(--bg-muted);
}
.antialised {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
@layer utilities {
.step {
counter-increment: step;
}
.step:before {
@apply absolute inline-flex h-9 w-9 items-center justify-center rounded-full border-4 border-background bg-muted text-center -indent-px font-mono text-base font-medium;
@apply ml-[-50px] mt-[-4px];
content: counter(step);
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
}
.link {
@apply inline-flex items-center gap-1 rounded-sm font-medium underline underline-offset-4 hover:text-foreground/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type="number"] {
-moz-appearance: textfield;
}
}
API Reference
The root component used to manage the state of the state of the popover.
Property | Type | Description |
---|---|---|
open $bindable | boolean | The open state of the link popover component. Default: false |
onOpenChange | function | A callback that fires when the open state changes. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
A component which toggles the opening and closing of the popover on press.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLButtonElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-state | enum | Whether the popover is open or closed. |
data-popover-trigger | '' | Present on the trigger element. |
The contents of the popover which are displayed when the popover is open.
Property | Type | Description |
---|---|---|
side | enum | The preferred side of the anchor to render the floating element against when open. Will be reversed when collisions occur. Default: bottom |
sideOffset | number | The distance in pixels from the anchor to the floating element. Default: 0 |
align | enum | The preferred alignment of the anchor to render the floating element against when open. This may change when collisions occur. Default: start |
alignOffset | number | The distance in pixels from the anchor to the floating element. Default: 0 |
arrowPadding | number | The amount in pixels of virtual padding around the viewport edges to check for overflow which will cause a collision. Default: 0 |
avoidCollisions | boolean | When Default: true |
collisionBoundary | union | A boundary element or array of elements to check for collisions against. Default: undefined |
collisionPadding | union | The amount in pixels of virtual padding around the viewport edges to check for overflow which will cause a collision. Default: 0 |
sticky | enum | The sticky behavior on the align axis. Default: partial |
hideWhenDetached | boolean | When Default: true |
updatePositionStrategy | enum | The strategy to use when updating the position of the content. When Default: optimized |
strategy | enum | The positioning strategy to use for the floating element. When Default: fixed |
preventScroll | boolean | When Default: false |
customAnchor | union | Use an element other than the trigger to anchor the content to. If provided, the content will be anchored to the provided element instead of the trigger. Default: null |
onInteractOutside | function | Callback fired when an outside interaction event occurs, which is a Default: undefined |
onFocusOutside | function | Callback fired when focus leaves the dismissible layer. You can call Default: undefined |
interactOutsideBehavior | enum | The behavior to use when an interaction occurs outside of the floating content. Default: close |
onEscapeKeydown | function | Callback fired when an escape keydown event occurs in the floating content. You can call Default: undefined |
escapeKeydownBehavior | enum | The behavior to use when an escape keydown event occurs in the floating content. Default: close |
onOpenAutoFocus | function | Event handler called when auto-focusing the content as it is opened. Can be prevented. Default: undefined |
onCloseAutoFocus | function | Event handler called when auto-focusing the content as it is closed. Can be prevented. Default: undefined |
trapFocus | boolean | Whether or not to trap the focus within the content when open. Default: true |
preventOverflowTextSelection | boolean | When Default: true |
forceMount | boolean | Whether or not to forcefully mount the content. This is useful if you want to use Svelte transitions or another animation library for the content. Default: false |
dir | enum | The reading direction of the app. Default: 'ltr' |
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-state | enum | Whether the popover is open or closed. |
data-popover-content | '' | Present on the content element. |
The contents of the popover which are displayed when the popover is open. (Static/No Floating UI)
Property | Type | Description |
---|---|---|
onInteractOutside | function | Callback fired when an outside interaction event occurs, which is a Default: undefined |
onFocusOutside | function | Callback fired when focus leaves the dismissible layer. You can call Default: undefined |
interactOutsideBehavior | enum | The behavior to use when an interaction occurs outside of the floating content. Default: close |
onEscapeKeydown | function | Callback fired when an escape keydown event occurs in the floating content. You can call Default: undefined |
escapeKeydownBehavior | enum | The behavior to use when an escape keydown event occurs in the floating content. Default: close |
onOpenAutoFocus | function | Event handler called when auto-focusing the content as it is opened. Can be prevented. Default: undefined |
onCloseAutoFocus | function | Event handler called when auto-focusing the content as it is closed. Can be prevented. Default: undefined |
trapFocus | boolean | Whether or not to trap the focus within the content when open. Default: true |
preventOverflowTextSelection | boolean | When Default: true |
preventScroll | boolean | When Default: false |
forceMount | boolean | Whether or not to forcefully mount the content. This is useful if you want to use Svelte transitions or another animation library for the content. Default: false |
dir | enum | The reading direction of the app. Default: 'ltr' |
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-state | enum | Whether the popover is open or closed. |
data-popover-content | '' | Present on the content element. |
A button which closes the popover when pressed and is typically placed in the content.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLButtonElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-popover-close | '' | Present on the close button. |
An optional arrow element which points to the trigger when the popover is open.
Property | Type | Description |
---|---|---|
width | number | The width of the arrow in pixels. Default: 8 |
height | number | The height of the arrow in pixels. Default: 8 |
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-arrow | '' | Present on the arrow element. |
data-popover-arrow | '' | Present on the arrow element. |