Learn how to customize the appearance of the Link Preview Component.
You can customize the component by passing additional attributes to the <link-metadata-preview>
tag.
Attribute | Type / Values | Default | What it does |
---|---|---|---|
data-url required | string | – | The 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-url | string | — | If 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-endpoint | string | API default | Point to your own metadata micro-service. String is prefixed to the encoded data-url . |
data-meta | JSON string | — | Skip 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.
There are 3 options to customize the appearance of the component using CSS variables:
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>
<!-- 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>
Variable | Purpose | Default value |
---|---|---|
--lm-border | Card border shorthand (color, width, style) | 1px solid #d7dce0 |
--lm-radius | Corner radius of the card | 8px ( 12px in card theme ) |
--lm-bg | Card background color | #fff |
--lm-shadow | Base box-shadow | 0 1px 3px 0 rgba(0,0,0,.1), 0 1px 2px 0 rgba(0,0,0,.06) |
--lm-hover-shadow | Shadow on hover | 0 4px 6px -1px rgba(0,0,0,.1), 0 2px 4px -1px rgba(0,0,0,.06) |
--lm-max-width | Max width of the card (horizontal) | 500px |
--lm-padding | Inner padding around text block | 12px |
--lm-title-color | Title text color | #111 |
--lm-title-size | Title font size | 1em |
--lm-desc-color | Description text color | #444 |
--lm-desc-size | Description font size | 0.875em |
--lm-url-color | URL line color | #555 |
--lm-url-size | URL font size | 0.75em |
--lm-branding-color | “Preview by …” line color | #777 |
--lm-branding-size | Branding font size | 0.65em |
--lm-fallback-bg | Background shown when no image is available | #f0f0f0 |
--lm-fallback-icon-color | Chain-link icon color in that fallback | #aaa |
--lm-fallback-icon-size | Size of that icon | 48px |
--lm-image-width | Width of the thumbnail in horizontal layout | 120px |
--lm-image-aspect-ratio-horizontal | Aspect ratio of that thumbnail | 1.91 / 1 |
--lm-image-aspect-ratio-vertical | Aspect ratio of the thumbnail in vertical layout | 16 / 9 |
--lm-image-height-vertical | Explicit height used on very small screens (CSS @media override) | 150px |
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.
<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.
Selector | What it is | Typical tweaks |
---|---|---|
.lm-card | The anchor that forms the whole card | border, radius, background, hover effects, CSS vars |
.lm-image-container | The thumbnail wrapper | aspect-ratio , background colour, object-fit policy |
img inside it | The fetched image | fancy filters (filter:grayscale(.6) ), object-fit |
.lm-default-icon | Chain-link SVG shown when image is missing | stroke / fill colour, size |
.lm-content | Text column | internal spacing, flex alignment |
.lm-title | Title line | colour, weight, font-size, text-decoration |
.lm-desc | Description (may be missing) | -webkit-line-clamp , colour |
.lm-url | Hostname line | colour, font-style |
.lm-branding | Branding tagline | hide, colour, font-style |
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%)'
);
});
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');
});
CSS vars are still the cleanest way to theme because:
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.
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.
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.