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 |
Data Attribute | Details |
---|
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 |
Data Attribute | Details |
---|