Link Preview
Displays a summarized preview of a linked content's details or information.
<script lang="ts">
import { Avatar, LinkPreview } from "bits-ui";
import CalendarBlank from "phosphor-svelte/lib/CalendarBlank";
import MapPin from "phosphor-svelte/lib/MapPin";
</script>
<LinkPreview.Root>
<LinkPreview.Trigger
href="https://x.com/huntabyte"
target="_blank"
rel="noreferrer noopener"
class="rounded-xs underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-black"
>
<Avatar.Root
class="data-[status=loaded]:border-foreground bg-muted text-muted-foreground h-12 w-12 rounded-full border border-transparent text-[17px] font-medium uppercase"
>
<div
class="flex h-full w-full items-center justify-center overflow-hidden rounded-full border-2 border-transparent"
>
<Avatar.Image src="/avatar-1.png" alt="@huntabyte" />
<Avatar.Fallback class="border-muted border">HB</Avatar.Fallback>
</div>
</Avatar.Root>
</LinkPreview.Trigger>
<LinkPreview.Content
class="border-muted bg-background shadow-popover w-[331px] rounded-xl border p-[17px]"
sideOffset={8}
>
<div class="flex space-x-4">
<Avatar.Root
class="data-[status=loaded]:border-foreground bg-muted text-muted-foreground h-12 w-12 rounded-full border border-transparent text-[17px] font-medium uppercase"
>
<div
class="flex h-full w-full items-center justify-center overflow-hidden rounded-full border-2 border-transparent"
>
<Avatar.Image src="/avatar-1.png" alt="@huntabyte" />
<Avatar.Fallback class="border-muted border">HB</Avatar.Fallback>
</div>
</Avatar.Root>
<div class="space-y-1 text-sm">
<h4 class="font-medium">@huntabyte</h4>
<p>I do things on the internet.</p>
<div
class="text-muted-foreground flex items-center gap-[21px] pt-2 text-xs"
>
<div class="flex items-center text-xs">
<MapPin class="mr-1 size-4" />
<span> FL, USA </span>
</div>
<div class="flex items-center text-xs">
<CalendarBlank class="mr-1 size-4" />
<span> Joined May 2020</span>
</div>
</div>
</div>
</div>
</LinkPreview.Content>
</LinkPreview.Root>
@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");
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@font-face {
font-family: "Cal Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/CalSans-SemiBold.woff2") format("woff2");
}
:root {
/* Colors */
--background: hsl(0 0% 100%);
--background-alt: hsl(0 0% 100%);
--foreground: hsl(0 0% 9%);
--foreground-alt: hsl(0 0% 32%);
--muted: hsl(240 5% 96%);
--muted-foreground: hsla(0 0% 9% / 0.4);
--border: hsl(240 6% 10%);
--border-input: hsla(240 6% 10% / 0.17);
--border-input-hover: hsla(240 6% 10% / 0.4);
--border-card: hsla(240 6% 10% / 0.1);
--dark: hsl(240 6% 10%);
--dark-10: hsla(240 6% 10% / 0.1);
--dark-40: hsla(240 6% 10% / 0.4);
--dark-04: hsla(240 6% 10% / 0.04);
--accent: hsl(204 94% 94%);
--accent-foreground: hsl(204 80% 16%);
--destructive: hsl(347 77% 50%);
--tertiary: hsl(37.7 92.1% 50.2%);
--line: hsl(0 0% 100%);
/* black */
--contrast: hsl(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: hsl(0 0% 5%);
--background-alt: hsl(0 0% 8%);
--foreground: hsl(0 0% 95%);
--foreground-alt: hsl(0 0% 70%);
--muted: hsl(240 4% 16%);
--muted-foreground: hsla(0 0% 100% / 0.4);
--border: hsl(0 0% 96%);
--border-input: hsla(0 0% 96% / 0.17);
--border-input-hover: hsla(0 0% 96% / 0.4);
--border-card: hsla(0 0% 96% / 0.1);
--dark: hsl(0 0% 96%);
--dark-40: hsl(0 0% 96% / 0.4);
--dark-10: hsl(0 0% 96% / 0.1);
--dark-04: hsl(0 0% 96% / 0.04);
--accent: hsl(204 90% 90%);
--accent-foreground: hsl(204 94% 94%);
--destructive: hsl(350 89% 60%);
--line: hsl(0 0% 9.02%);
--tertiary: hsl(61.3 100% 82.2%);
/* white */
--contrast: hsl(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);
}
@theme inline {
--color-background: var(--background);
--color-background-alt: var(--background-alt);
--color-foreground: var(--foreground);
--color-foreground-alt: var(--foreground-alt);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border-card);
--color-border-input: var(--border-input);
--color-border-input-hover: var(--border-input-hover);
--color-border-card: var(--border-card);
--color-dark: var(--dark);
--color-dark-10: var(--dark-10);
--color-dark-40: var(--dark-40);
--color-dark-04: var(--dark-04);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-tertiary: var(--tertiary);
--color-line: var(--line);
--color-contrast: var(--contrast);
--shadow-mini: var(--shadow-mini);
--shadow-mini-inset: var(--shadow-mini-inset);
--shadow-popover: var(--shadow-popover);
--shadow-kbd: var(--shadow-kbd);
--shadow-btn: var(--shadow-btn);
--shadow-card: var(--shadow-card);
--shadow-date-field-focus: var(--shadow-date-field-focus);
--text-xxs: 10px;
--radius-card: 16px;
--radius-card-lg: 20px;
--radius-card-sm: 10px;
--radius-input: 9px;
--radius-button: 5px;
--radius-5px: 5px;
--radius-9px: 9px;
--radius-10px: 10px;
--radius-15px: 15px;
--spacing-input: 3rem;
--spacing-input-sm: 2.5rem;
--breakpoint-desktop: 1440px;
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-caret-blink: caret-blink 1s ease-out infinite;
--animate-scale-in: scale-in 0.2s ease;
--animate-scale-out: scale-out 0.15s ease;
--animate-fade-in: fade-in 0.2s ease;
--animate-fade-out: fade-out 0.15s ease;
--animate-enter-from-left: enter-from-left 0.2s ease;
--animate-enter-from-right: enter-from-right 0.2s ease;
--animate-exit-to-left: exit-to-left 0.2s ease;
--animate-exit-to-right: exit-to-right 0.2s ease;
--font-sans: "Inter", "sans-serif";
--font-mono: "Source Code Pro", "monospace";
--font-alt: "Courier", "sans-serif";
--font-display: "Cal Sans", "sans-serif";
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--bits-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--bits-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes caret-blink {
0%,
70%,
100% {
opacity: 1;
}
20%,
50% {
opacity: 0;
}
}
@keyframes enter-from-right {
from {
opacity: 0;
transform: translateX(200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes enter-from-left {
from {
opacity: 0;
transform: translateX(-200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes exit-to-right {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(200px);
}
}
@keyframes exit-to-left {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-200px);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: rotateX(-10deg) scale(0.9);
}
to {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
}
@keyframes scale-out {
from {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
to {
opacity: 0;
transform: rotateX(-10deg) scale(0.95);
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-border-card, currentColor);
}
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
scrollbar-color: var(--bg-muted);
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
::selection {
background: #fdffa4;
color: black;
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:ring-foreground focus-visible:ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1;
}
}
.link {
@apply hover:text-foreground/80 focus-visible:ring-foreground focus-visible:ring-offset-background rounded-xs focus-visible:outline-hidden inline-flex items-center gap-1 font-medium underline underline-offset-4 focus-visible:ring-2 focus-visible:ring-offset-2;
}
}
Overview
A component that lets users preview a link before they decide to follow it. This is useful for providing non-essential context or additional information about a link without having to navigate away from the current page.
A note about mobile devices!
This component is only intended to be used with a mouse or other pointing device. It doesn't respond to touch events, and the preview content cannot be accessed via the keyboard. On touch devices, the link will be followed immediately. As it is not accessible to all users, the preview should not contain vital information.
Structure
<script lang="ts">
import { LinkPreview } from "bits-ui";
</script>
<LinkPreview.Root>
<LinkPreview.Trigger />
<LinkPreview.Content />
</LinkPreview.Root>
Managing Open State
This section covers how to manage the open state of the component.
Two-Way Binding
Use bind:open for simple, automatic state synchronization:
<script lang="ts">
import { LinkPreview } from "bits-ui";
let isOpen = $state(false);
</script>
<button onclick={() => (isOpen = true)}>Open Link Preview</button>
<LinkPreview.Root bind:open={isOpen}>
<!-- ... -->
</LinkPreview.Root>
Fully Controlled
Use a Function Binding for complete control over the state's reads and writes.
<script lang="ts">
import { LinkPreview } from "bits-ui";
let myOpen = $state(false);
function getOpen() {
return myOpen;
}
function setOpen(newOpen: boolean) {
myOpen = newOpen;
}
</script>
<LinkPreview.Root bind:open={getOpen, setOpen}>
<!-- ... -->
</LinkPreview.Root>
Opt-out of Floating UI
When you use the LinkPreview.Content component, Bits UI uses Floating UI to position the content relative to the trigger, similar to other popover-like components.
You can opt-out of this behavior by instead using the LinkPreview.ContentStatic component. This component does not use Floating UI and leaves positioning the content entirely up to you.
<LinkPreview.Root>
<LinkPreview.Trigger />
<LinkPreview.ContentStatic>
<!-- ... -->
</LinkPreview.ContentStatic>
</LinkPreview.Root>
Heads up!
The LinkPreview.Arrow component is designed to be used with Floating UI and LinkPreview.Content, so you may experience unexpected behavior if you attempt to use it with LinkPreview.ContentStatic.
Custom Anchor
By default, the LinkPreview.Content is anchored to the LinkPreview.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 LinkPreview.Content component.
<script lang="ts">
import { LinkPreview } from "bits-ui";
let customAnchor = $state<HTMLElement>(null!);
</script>
<div bind:this={customAnchor}></div>
<LinkPreview.Root>
<LinkPreview.Trigger />
<LinkPreview.Content {customAnchor}>
<!-- ... -->
</LinkPreview.Content>
</LinkPreview.Root>
Svelte Transitions
You can use the forceMount prop along with the child snippet to forcefully mount the LinkPreview.Content component to use Svelte Transitions or another animation library that requires more control.
<script lang="ts">
import { LinkPreview } from "bits-ui";
import { fly } from "svelte/transition";
</script>
<LinkPreview.Content forceMount>
{#snippet child({ wrapperProps, props, open })}
{#if open}
<div {...wrapperProps}>
<div {...props} transition:fly>
<!-- ... -->
</div>
</div>
{/if}
{/snippet}
</LinkPreview.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 { Avatar, LinkPreview } from "bits-ui";
import CalendarBlank from "phosphor-svelte/lib/CalendarBlank";
import MapPin from "phosphor-svelte/lib/MapPin";
import { fly } from "svelte/transition";
let loadingStatusTrigger: Avatar.RootProps["loadingStatus"] =
$state("loading");
let loadingStatusContent: Avatar.RootProps["loadingStatus"] =
$state("loading");
</script>
<LinkPreview.Root>
<LinkPreview.Trigger
href="https://github.com/sveltejs"
target="_blank"
rel="noreferrer noopener"
class="rounded-xs underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-black"
>
<Avatar.Root
bind:loadingStatus={loadingStatusTrigger}
class="h-12 w-12 rounded-full border {loadingStatusTrigger === 'loaded'
? 'border-foreground'
: 'border-transparent'} bg-muted text-muted-foreground text-[17px] font-medium uppercase"
>
<div
class="flex h-full w-full items-center justify-center overflow-hidden rounded-full border-2 border-transparent"
>
<Avatar.Image src="/avatar-1.png" alt="@huntabyte" />
<Avatar.Fallback class="border-muted border">HB</Avatar.Fallback>
</div>
</Avatar.Root>
</LinkPreview.Trigger>
<LinkPreview.Content
class="border-muted bg-background shadow-popover w-[331px] rounded-xl border p-[17px]"
sideOffset={8}
forceMount
>
{#snippet child({ open, props, wrapperProps })}
{#if open}
<div {...wrapperProps}>
<div {...props} transition:fly={{ duration: 300 }}>
<div class="flex space-x-4">
<Avatar.Root
bind:loadingStatus={loadingStatusContent}
class="h-12 w-12 rounded-full border {loadingStatusContent ===
'loaded'
? 'border-foreground'
: 'border-transparent'} bg-muted text-muted-foreground text-[17px] font-medium uppercase"
>
<div
class="flex h-full w-full items-center justify-center overflow-hidden rounded-full border-2 border-transparent"
>
<Avatar.Image src="/avatar-1.png" alt="@huntabyte" />
<Avatar.Fallback class="border-muted border"
>HB</Avatar.Fallback
>
</div>
</Avatar.Root>
<div class="space-y-1 text-sm">
<h4 class="font-medium">@huntabyte</h4>
<p>I do things on the internet.</p>
<div
class="text-muted-foreground flex items-center gap-[21px] pt-2 text-xs"
>
<div class="flex items-center text-xs">
<MapPin class="mr-1 size-4" />
<span> FL, USA </span>
</div>
<div class="flex items-center text-xs">
<CalendarBlank class="mr-1 size-4" />
<span> Joined May 2020</span>
</div>
</div>
</div>
</div>
</div>
</div>
{/if}
{/snippet}
</LinkPreview.Content>
</LinkPreview.Root>
@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");
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@font-face {
font-family: "Cal Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/CalSans-SemiBold.woff2") format("woff2");
}
:root {
/* Colors */
--background: hsl(0 0% 100%);
--background-alt: hsl(0 0% 100%);
--foreground: hsl(0 0% 9%);
--foreground-alt: hsl(0 0% 32%);
--muted: hsl(240 5% 96%);
--muted-foreground: hsla(0 0% 9% / 0.4);
--border: hsl(240 6% 10%);
--border-input: hsla(240 6% 10% / 0.17);
--border-input-hover: hsla(240 6% 10% / 0.4);
--border-card: hsla(240 6% 10% / 0.1);
--dark: hsl(240 6% 10%);
--dark-10: hsla(240 6% 10% / 0.1);
--dark-40: hsla(240 6% 10% / 0.4);
--dark-04: hsla(240 6% 10% / 0.04);
--accent: hsl(204 94% 94%);
--accent-foreground: hsl(204 80% 16%);
--destructive: hsl(347 77% 50%);
--tertiary: hsl(37.7 92.1% 50.2%);
--line: hsl(0 0% 100%);
/* black */
--contrast: hsl(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: hsl(0 0% 5%);
--background-alt: hsl(0 0% 8%);
--foreground: hsl(0 0% 95%);
--foreground-alt: hsl(0 0% 70%);
--muted: hsl(240 4% 16%);
--muted-foreground: hsla(0 0% 100% / 0.4);
--border: hsl(0 0% 96%);
--border-input: hsla(0 0% 96% / 0.17);
--border-input-hover: hsla(0 0% 96% / 0.4);
--border-card: hsla(0 0% 96% / 0.1);
--dark: hsl(0 0% 96%);
--dark-40: hsl(0 0% 96% / 0.4);
--dark-10: hsl(0 0% 96% / 0.1);
--dark-04: hsl(0 0% 96% / 0.04);
--accent: hsl(204 90% 90%);
--accent-foreground: hsl(204 94% 94%);
--destructive: hsl(350 89% 60%);
--line: hsl(0 0% 9.02%);
--tertiary: hsl(61.3 100% 82.2%);
/* white */
--contrast: hsl(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);
}
@theme inline {
--color-background: var(--background);
--color-background-alt: var(--background-alt);
--color-foreground: var(--foreground);
--color-foreground-alt: var(--foreground-alt);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border-card);
--color-border-input: var(--border-input);
--color-border-input-hover: var(--border-input-hover);
--color-border-card: var(--border-card);
--color-dark: var(--dark);
--color-dark-10: var(--dark-10);
--color-dark-40: var(--dark-40);
--color-dark-04: var(--dark-04);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-tertiary: var(--tertiary);
--color-line: var(--line);
--color-contrast: var(--contrast);
--shadow-mini: var(--shadow-mini);
--shadow-mini-inset: var(--shadow-mini-inset);
--shadow-popover: var(--shadow-popover);
--shadow-kbd: var(--shadow-kbd);
--shadow-btn: var(--shadow-btn);
--shadow-card: var(--shadow-card);
--shadow-date-field-focus: var(--shadow-date-field-focus);
--text-xxs: 10px;
--radius-card: 16px;
--radius-card-lg: 20px;
--radius-card-sm: 10px;
--radius-input: 9px;
--radius-button: 5px;
--radius-5px: 5px;
--radius-9px: 9px;
--radius-10px: 10px;
--radius-15px: 15px;
--spacing-input: 3rem;
--spacing-input-sm: 2.5rem;
--breakpoint-desktop: 1440px;
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-caret-blink: caret-blink 1s ease-out infinite;
--animate-scale-in: scale-in 0.2s ease;
--animate-scale-out: scale-out 0.15s ease;
--animate-fade-in: fade-in 0.2s ease;
--animate-fade-out: fade-out 0.15s ease;
--animate-enter-from-left: enter-from-left 0.2s ease;
--animate-enter-from-right: enter-from-right 0.2s ease;
--animate-exit-to-left: exit-to-left 0.2s ease;
--animate-exit-to-right: exit-to-right 0.2s ease;
--font-sans: "Inter", "sans-serif";
--font-mono: "Source Code Pro", "monospace";
--font-alt: "Courier", "sans-serif";
--font-display: "Cal Sans", "sans-serif";
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--bits-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--bits-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes caret-blink {
0%,
70%,
100% {
opacity: 1;
}
20%,
50% {
opacity: 0;
}
}
@keyframes enter-from-right {
from {
opacity: 0;
transform: translateX(200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes enter-from-left {
from {
opacity: 0;
transform: translateX(-200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes exit-to-right {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(200px);
}
}
@keyframes exit-to-left {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-200px);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: rotateX(-10deg) scale(0.9);
}
to {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
}
@keyframes scale-out {
from {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
to {
opacity: 0;
transform: rotateX(-10deg) scale(0.95);
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-border-card, currentColor);
}
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
scrollbar-color: var(--bg-muted);
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
::selection {
background: #fdffa4;
color: black;
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:ring-foreground focus-visible:ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1;
}
}
.link {
@apply hover:text-foreground/80 focus-visible:ring-foreground focus-visible:ring-offset-background rounded-xs focus-visible:outline-hidden inline-flex items-center gap-1 font-medium underline underline-offset-4 focus-visible:ring-2 focus-visible:ring-offset-2;
}
}
API Reference
The root component used to manage the state of the state of the link preview.
| Property | Details |
|---|---|
open | |
onOpenChange | |
onOpenChangeComplete | |
openDelay | |
closeDelay | |
disabled | |
ignoreNonKeyboardFocus | |
children |
A component which triggers the opening and closing of the link preview on hover or focus.
| Property | Details |
|---|---|
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-state | |
data-link-preview-trigger |
The contents of the link preview which are displayed when the preview is open.
| Property | Details |
|---|---|
side | |
sideOffset | |
align | |
alignOffset | |
arrowPadding | |
avoidCollisions | |
collisionBoundary | |
collisionPadding | |
sticky | |
hideWhenDetached | |
updatePositionStrategy | |
strategy | |
preventScroll | |
customAnchor | |
onInteractOutside | |
onFocusOutside | |
interactOutsideBehavior | |
onEscapeKeydown | |
escapeKeydownBehavior | |
onOpenAutoFocus | |
onCloseAutoFocus | |
trapFocus | |
dir | |
forceMount | |
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-state | |
data-link-preview-content |
| CSS Variable | Details |
|---|---|
--bits-link-preview-content-transform-origin | |
--bits-link-preview-content-available-width | |
--bits-link-preview-content-available-height | |
--bits-link-preview-anchor-width | |
--bits-link-preview-anchor-height |
The contents of the link preview which are displayed when the preview is open. (Static/No Floating UI)
| Property | Details |
|---|---|
onInteractOutside | |
onFocusOutside | |
interactOutsideBehavior | |
onEscapeKeydown | |
escapeKeydownBehavior | |
onOpenAutoFocus | |
onCloseAutoFocus | |
trapFocus | |
dir | |
forceMount | |
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-state | |
data-link-preview-content |
An optional arrow element which points to the trigger when the preview is open.
| Property | Details |
|---|---|
width | |
height | |
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-link-preview-arrow |
When used, will render the link preview content into the body or custom to element when open
| Property | Details |
|---|---|
to | |
disabled | |
children |