Getting Started

Customize the Preview Component

Learn how to customize the appearance of the Link Preview Component.

Attributes

You can customize the component by passing additional attributes to the <link-metadata-preview> tag.

AttributeType / ValuesDefaultWhat it does
data-url requiredstringThe page you want to preview.
data-theme"light", "dark", "card""light"Picks one of the preset colour systems (next section).
data-orientation"horizontal", "vertical""horizontal"Layout: image on the left vs. image on top.
data-show-image"true", "false""true"Show or hide the image.
data-show-branding"true", "false""true"Show or hide the “Preview by LinkMetadata.com” line.
data-original-urlstringIf you pass a canonicalised link to the API but want the card to display the original hostname (useful for campaign links, shortened URLs, etc.).
data-api-endpointstringAPI defaultPoint to your own metadata micro-service. String is prefixed to the encoded data-url.
data-metaJSON stringSkip the fetch entirely and hydrate from pre-fetched metadata (handy for SSR).

All attributes are reactive: change them at runtime and the component re-renders automatically.

Use CSS variables

There are 3 options to customize the appearance of the component using CSS variables:

  1. Inline: Use the style attribute to apply custom styles to this tag only.
<!-- Use the `style` attribute to apply custom styles to this tag only -->
<link-metadata-preview
  data-url="https://news.ycombinator.com"
  data-theme="dark"
  style="
    --lm-title-color:#38bdf8;
    --lm-border:1px solid #0ea5e9;
    --lm-radius:16px;
    --lm-shadow:0 10px 20px -5px rgba(0,0,0,.45);
  ">
</link-metadata-preview>
  1. Global: Drop this once, anywhere in your document or a stylesheet to apply custom styles to all tags.
<!-- drop this once, anywhere in your document to apply custom styles to all tags -->
<style>
  /* Applied to the host; trickles into every shadow root */
  link-metadata-preview {
    --lm-radius: 10px;
    --lm-hover-shadow: 0 8px 16px -4px rgba(0,0,0,.30);
    --lm-image-aspect-ratio-horizontal: 4 / 3;
  }
</style>
  1. JavaScript: For those who need more than a couple of CSS variables. See below section on Advanced usage with JavaScript

Available CSS variables

VariablePurposeDefault value
--lm-borderCard border shorthand (color, width, style)1px solid #d7dce0
--lm-radiusCorner radius of the card8px ( 12px in card theme )
--lm-bgCard background color#fff
--lm-shadowBase box-shadow0 1px 3px 0 rgba(0,0,0,.1), 0 1px 2px 0 rgba(0,0,0,.06)
--lm-hover-shadowShadow on hover0 4px 6px -1px rgba(0,0,0,.1), 0 2px 4px -1px rgba(0,0,0,.06)
--lm-max-widthMax width of the card (horizontal)500px
--lm-paddingInner padding around text block12px
--lm-title-colorTitle text color#111
--lm-title-sizeTitle font size1em
--lm-desc-colorDescription text color#444
--lm-desc-sizeDescription font size0.875em
--lm-url-colorURL line color#555
--lm-url-sizeURL font size0.75em
--lm-branding-color“Preview by …” line color#777
--lm-branding-sizeBranding font size0.65em
--lm-fallback-bgBackground shown when no image is available#f0f0f0
--lm-fallback-icon-colorChain-link icon color in that fallback#aaa
--lm-fallback-icon-sizeSize of that icon48px
--lm-image-widthWidth of the thumbnail in horizontal layout120px
--lm-image-aspect-ratio-horizontalAspect ratio of that thumbnail1.91 / 1
--lm-image-aspect-ratio-verticalAspect ratio of the thumbnail in vertical layout16 / 9
--lm-image-height-verticalExplicit height used on very small screens (CSS @media override)150px

How the presets interact

  • light: sets every colour variable above to neutral greys on white.
  • dark: overrides the colour variables with slate-tones on near-black, leaves sizing alone.
  • card: same palette as light, removes --lm-border, bumps --lm-radius to 12px, and uses a gentler off-white --lm-bg.

Anything not touched by a theme keeps the fallback you see in the table.

Advanced usage with JavaScript

Shadow-DOM basics

<link-metadata-preview> attaches its shadow root in open mode, so you can do:

const el   = document.querySelector('link-metadata-preview');
const root = el.shadowRoot;        // this is allowed

All styleable nodes live inside that shadow tree; nothing is exposed in light-DOM except the custom-element itself.

link-metadata-preview
└─ #shadowRoot
   ├─ a.lm-card            ← main wrapper
   │   ├─ div.lm-image-container
   │   │   └─ img           OR div.lm-default-icon-container → svg.lm-default-icon
   │   └─ div.lm-content
   │       ├─ span.lm-title
   │       ├─ span.lm-desc       (may be empty)
   │       ├─ span.lm-url
   │       └─ div.lm-branding    (can be hidden)

Is the structure stable? Yes. These class names (.lm-*) are considered public contract for styling; when we ship a breaking change we’ll bump the major version.

Selectors you can rely on

SelectorWhat it isTypical tweaks
.lm-cardThe anchor that forms the whole cardborder, radius, background, hover effects, CSS vars
.lm-image-containerThe thumbnail wrapperaspect-ratio, background colour, object-fit policy
img inside itThe fetched imagefancy filters (filter:grayscale(.6)), object-fit
.lm-default-iconChain-link SVG shown when image is missingstroke / fill colour, size
.lm-contentText columninternal spacing, flex alignment
.lm-titleTitle linecolour, weight, font-size, text-decoration
.lm-descDescription (may be missing)-webkit-line-clamp, colour
.lm-urlHostname linecolour, font-style
.lm-brandingBranding taglinehide, colour, font-style

One-off tweak

await customElements.whenDefined('link-metadata-preview');

const root = document.querySelector('link-metadata-preview').shadowRoot;

// 1) change a CSS custom property on the card
root.querySelector('.lm-card')
    .style.setProperty('--lm-title-color', '#22c55e');   // lime-green

// 2) hide the branding line entirely
root.querySelector('.lm-branding').style.display = 'none';

// 3) make the image grayscale on hover
root.querySelector('.lm-card').addEventListener('pointerenter', e => {
  root.querySelector('.lm-image-container img')?.style.setProperty(
    'filter', 'grayscale(100%)'
  );
});

Batch-tuning every instance on the page

async function styleAllPreviews(cb) {
  await customElements.whenDefined('link-metadata-preview');
  document.querySelectorAll('link-metadata-preview').forEach(el => cb(el));
}

// Example: give every preview a purple border that matches your brand
styleAllPreviews(el => {
  const card = el.shadowRoot.querySelector('.lm-card');
  card.style.setProperty('--lm-border', '2px solid rebeccapurple');
});

Combining JS with design tokens

CSS vars are still the cleanest way to theme because:

  1. They inherit down shadow-DOM boundaries.
  2. They let you support light/dark automatically.

A nice pattern is to set defaults with CSS, then let JS toggle a single var:

/* global.css */
:root {
  --preview-accent: #2563eb;          /* blue by default */
}

@media (prefers-color-scheme: dark) {
  :root { --preview-accent: #38bdf8; } /* lighter blue in dark-mode */
}
styleAllPreviews(el => {
  el.shadowRoot.querySelector('.lm-card')
     .style.setProperty('--lm-border', `1px solid var(--preview-accent)`);
});

Dark-mode users now get the right colour automatically.

When you really need to rewrite markup

Because the preview is a standard anchor element, you can:

const a = el.shadowRoot.querySelector('.lm-card');
// Turn the whole thing into a div (loses keyboard a11y—be careful!)
a.replaceWith(a.cloneNode(true));
a.outerHTML = a.outerHTML.replace('<a ', '<div ').replace('</a>', '</div>');

…but only do this if you own the environment (e.g. WebView, kiosk) and are willing to take on accessibility responsibilities. If you must remove the link, add tabindex="0" and ARIA role links, or keep the <a> and block pointer events.

Cheat-sheet

await customElements.whenDefined('link-metadata-preview');

const el    = document.querySelector('link-metadata-preview');
const root  = el.shadowRoot;                   // open mode → accessible
const card  = root.querySelector('.lm-card');  // main wrapper
const image = root.querySelector('.lm-image-container img');
const title = root.querySelector('.lm-title');
const desc  = root.querySelector('.lm-desc');

// Change colours via CSS var
card.style.setProperty('--lm-bg', '#fdf6e3');

// Change element-level style
title.style.textTransform = 'uppercase';

// Hide description if you decide it is redundant
desc?.remove();

With those selectors and patterns you can script the component as freely as anything you build yourself—but without giving up the sandboxing and ready-made accessibility the web component provides.

Customize the Preview Component