Add Image label component (#3267)

This commit is contained in:
Paweł Kuna authored and GitHub committed 2026-10-05 23:24:15 +02:00
1 parent f7c848a938
commit 681b6eb671
10 files changed
+480

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
---
"@tabler/core": minor
"@tabler/preview": minor
"@tabler/docs": minor
---
Added the image label component: `.img-labeled` and `.img-label` pin a short text or an icon to a corner of an image.
+14
View File
@@ -1279,6 +1279,20 @@ $ribbon-bookmark-border-size: 1rem !default;
$ribbon-bookmark-notch-width: 0.5rem !default;
// scss-docs-end ribbon-variables
// scss-docs-start img-label-variables
$img-label-offset: 0.375rem !default;
$img-label-padding-y: 0.25rem !default;
$img-label-padding-x: 0.375rem !default;
$img-label-gap: 0.25rem !default;
$img-label-font-size: $h5-font-size !default;
$img-label-font-weight: var(--font-weight-medium) !default;
$img-label-color: $white !default;
$img-label-bg: rgba($black, 0.6) !default;
$img-label-border-radius: var(--border-radius-sm) !default;
$img-label-backdrop-filter: blur(4px) !default;
$img-label-icon-size: 0.75rem !default;
// scss-docs-end img-label-variables
// Accordion
// scss-docs-start accordion-variables
$accordion-padding-y: 1rem !default;
+61
View File
@@ -21,3 +21,64 @@
.img-bg {
background: no-repeat center/cover;
}
// A label pinned to a corner of an image, such as "AI", "New" or a video length
.img-labeled {
position: relative;
display: inline-block;
max-width: 100%;
vertical-align: middle;
}
.img-label {
// scss-docs-start img-label-css-vars
--img-label-offset: #{$img-label-offset};
--img-label-padding-y: #{$img-label-padding-y};
--img-label-padding-x: #{$img-label-padding-x};
--img-label-gap: #{$img-label-gap};
--img-label-font-size: #{$img-label-font-size};
--img-label-font-weight: #{$img-label-font-weight};
--img-label-color: #{$img-label-color};
--img-label-bg: #{$img-label-bg};
--img-label-border-radius: #{$img-label-border-radius};
--img-label-backdrop-filter: #{$img-label-backdrop-filter};
--img-label-icon-size: #{$img-label-icon-size};
// scss-docs-end img-label-css-vars
position: absolute;
inset-block-end: var(--img-label-offset);
inset-inline-end: var(--img-label-offset);
z-index: 1;
display: inline-flex;
gap: var(--img-label-gap);
align-items: center;
max-width: calc(100% - 2 * var(--img-label-offset));
padding: var(--img-label-padding-y) var(--img-label-padding-x);
font-size: var(--img-label-font-size);
font-weight: var(--img-label-font-weight);
line-height: 1;
color: var(--img-label-color);
white-space: nowrap;
pointer-events: none;
background: var(--img-label-bg);
backdrop-filter: var(--img-label-backdrop-filter);
@include border-radius(var(--img-label-border-radius));
.icon {
--icon-size: var(--img-label-icon-size);
}
}
// A label that holds only an icon: equal padding on all sides makes it a square
.img-label-icon {
--img-label-padding-x: var(--img-label-padding-y);
}
.img-label-top-start,
.img-label-top-end {
inset-block: var(--img-label-offset) auto;
}
.img-label-top-start,
.img-label-bottom-start {
inset-inline: var(--img-label-offset) auto;
}
+134
View File
@@ -0,0 +1,134 @@
---
title: Image label
summary: An image label is a short text or an icon pinned to a corner of an image, such as "AI", "New" or a video length.
description: Pin a short label to a corner of an image with the image label component. Change the corner and the color, or add an icon.
related: [/ui/components/ribbon, /ui/components/badge]
source: core/scss/ui/_images.scss
css-vars: img-label
classnames:
component:
- class: img-labeled
desc: Wrapper around the image that the labels are positioned in
part:
- class: img-label
desc: The label, in the bottom end corner by default
modifier:
- class: img-label-icon
desc: Square label for a single icon, with equal padding on all sides
direction:
- class: img-label-top-start
desc: Moves the label to the top start corner
- class: img-label-top-end
desc: Moves the label to the top end corner
- class: img-label-bottom-start
desc: Moves the label to the bottom start corner
color:
- class: bg-{color}
desc: Any base color, for example bg-red, used with the matching text-red-fg
---
import Example from '@components/Example.astro';
import Icon from '@ui/Icon.astro';
## Default markup
Wrap the image in an `.img-labeled` element and put an `.img-label` next to the image. The label sits in the bottom end corner, on a dark, see-through background that stays readable on any photo.
<Example centered>
<div class="img-labeled">
<img src="/static/photos/book-on-the-grass.jpg" class="rounded" width="240" alt="A book on the grass" />
<span class="img-label">AI</span>
</div>
</Example>
## Position
Move the label to another corner with `.img-label-top-start`, `.img-label-top-end` or `.img-label-bottom-start`. The corners follow the text direction, so start and end swap in right-to-left layouts.
<Example centered>
<div class="d-flex flex-wrap gap-3">
<div class="img-labeled">
<img src="/static/photos/a-woman-works-on-a-laptop-at-home.jpg" class="rounded" width="160" alt="A woman working on a laptop" />
<span class="img-label img-label-top-start">Top start</span>
</div>
<div class="img-labeled">
<img src="/static/photos/a-woman-works-on-a-laptop-at-home.jpg" class="rounded" width="160" alt="A woman working on a laptop" />
<span class="img-label img-label-top-end">Top end</span>
</div>
<div class="img-labeled">
<img src="/static/photos/a-woman-works-on-a-laptop-at-home.jpg" class="rounded" width="160" alt="A woman working on a laptop" />
<span class="img-label img-label-bottom-start">Bottom start</span>
</div>
<div class="img-labeled">
<img src="/static/photos/a-woman-works-on-a-laptop-at-home.jpg" class="rounded" width="160" alt="A woman working on a laptop" />
<span class="img-label">Bottom end</span>
</div>
</div>
</Example>
## With an icon
A label can hold an icon, a text, or both. The icon is scaled down to match the label text. Add `.img-label-icon` to a label with only an icon to make it a square.
<Example centered>
<div class="d-flex flex-wrap gap-3">
<div class="img-labeled">
<img src="/static/photos/brainstorming-session-with-creative-designers.jpg" class="rounded" width="200" alt="A brainstorming session" />
<span class="img-label"><Icon name="player-play" /> 1:24</span>
</div>
<div class="img-labeled">
<img src="/static/photos/blond-using-her-laptop-at-her-bedroom.jpg" class="rounded" width="200" alt="A woman using a laptop" />
<span class="img-label img-label-icon"><Icon name="crown" /><span class="visually-hidden">Premium</span></span>
</div>
</div>
</Example>
## Colors
Change the color with a background utility and its matching text color, for example `.bg-red` with `.text-red-fg`.
<Example centered>
<div class="d-flex flex-wrap gap-3">
<div class="img-labeled">
<img src="/static/photos/a-visit-to-the-bookstore.jpg" class="rounded" width="160" alt="A visit to the bookstore" />
<span class="img-label bg-primary text-primary-fg">New</span>
</div>
<div class="img-labeled">
<img src="/static/photos/a-visit-to-the-bookstore.jpg" class="rounded" width="160" alt="A visit to the bookstore" />
<span class="img-label bg-red text-red-fg">-20%</span>
</div>
<div class="img-labeled">
<img src="/static/photos/a-visit-to-the-bookstore.jpg" class="rounded" width="160" alt="A visit to the bookstore" />
<span class="img-label bg-green text-green-fg">Free</span>
</div>
</div>
</Example>
## Multiple labels
Put several labels on one image, each in its own corner.
<Example centered>
<div class="img-labeled">
<img src="/static/photos/brainstorming-session-with-creative-designers-2.jpg" class="rounded" width="320" alt="Designers at a brainstorming session" />
<span class="img-label img-label-top-start bg-red text-red-fg">Live</span>
<span class="img-label img-label-top-end"><Icon name="lock" /> Members</span>
<span class="img-label">4K</span>
</div>
</Example>
## Custom look
Set the `--tblr-img-label-*` variables on the label to change its offset, size or background. Set `--tblr-img-label-backdrop-filter` to `none` to turn off the blur.
<Example centered>
<div class="img-labeled">
<img src="/static/photos/book-on-the-grass.jpg" class="rounded" width="240" alt="A book on the grass" />
<span class="img-label" style="--tblr-img-label-offset: 0.75rem; --tblr-img-label-font-size: 0.875rem; --tblr-img-label-bg: rgba(255, 255, 255, 0.8); --tblr-img-label-color: #000">Sponsored</span>
</div>
</Example>
## Accessibility
- The label is real text, so screen readers read it after the image. For a short form such as "AI", add the full meaning in a `.visually-hidden` element, for example "AI generated".
- A label with only an icon needs a `.visually-hidden` text that names it.
- The label does not take clicks. When the image is a link, a click on the label goes to the link.
+148
View File
@@ -0,0 +1,148 @@
---
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
import Card from '@ui/Card.astro'
import CardBody from '@ui/CardBody.astro'
import CardTitle from '@ui/CardTitle.astro'
import CardSubtitle from '@ui/CardSubtitle.astro'
import DocsLink from '@ui/DocsLink.astro'
import Icon from '@ui/Icon.astro'
import ImageLabel from '@ui/ImageLabel.astro'
import { staticPath } from '@shared/lib/assets'
const photo = (file: string) => staticPath(`photos/${file}`)
const positions = [
{ position: 'top-start', label: 'Top start' },
{ position: 'top-end', label: 'Top end' },
{ position: 'bottom-start', label: 'Bottom start' },
{ position: 'bottom-end', label: 'Bottom end' },
] as const
const colors = ['primary', 'red', 'green', 'yellow']
const products = [
{ name: 'Reading corner', price: '$129', file: 'book-on-the-grass.jpg', label: 'AI' },
{ name: 'Designer sofa', price: '$849', file: 'blue-sofa-with-pillows-in-a-designer-living-room-interior.jpg', label: 'New' },
{ name: 'Bedside stool', price: '$59', file: 'books-and-purple-flowers-on-a-wooden-stool-by-the-bed.jpg', label: '-20%', color: 'red' },
]
---
<DefaultLayout title="Image labels" pageMenu="base.image-labels" description="Image labels pin a short text or an icon to a corner of an image.">
<DocsLink slot="page-header-actions" path="/ui/components/image-label" />
<div class="row row-cards">
<div class="col-md-6 col-lg-4">
<Card>
<CardBody>
<CardTitle>Basic</CardTitle>
<CardSubtitle>Wrap the image in <code>.img-labeled</code> and add an <code>.img-label</code> next to it.</CardSubtitle>
<div class="img-labeled">
<img src={photo('book-on-the-grass.jpg')} class="rounded" width="240" alt="A book on the grass" />
<ImageLabel>AI</ImageLabel>
</div>
</CardBody>
</Card>
</div>
<div class="col-md-6 col-lg-4">
<Card>
<CardBody>
<CardTitle>With an icon</CardTitle>
<CardSubtitle>A label can hold an icon, a text, or both. Add <code>.img-label-icon</code> to a label with only an icon.</CardSubtitle>
<div class="d-flex flex-wrap gap-3">
<div class="img-labeled">
<img src={photo('brainstorming-session-with-creative-designers.jpg')} class="rounded" width="160" alt="A brainstorming session" />
<ImageLabel><Icon name="player-play" /> 1:24</ImageLabel>
</div>
<div class="img-labeled">
<img src={photo('blond-using-her-laptop-at-her-bedroom.jpg')} class="rounded" width="160" alt="A woman using a laptop" />
<ImageLabel icon><Icon name="crown" /><span class="visually-hidden">Premium</span></ImageLabel>
</div>
</div>
</CardBody>
</Card>
</div>
<div class="col-md-6 col-lg-4">
<Card>
<CardBody>
<CardTitle>Colors</CardTitle>
<CardSubtitle>Add a background utility such as <code>.bg-red</code> with its <code>.text-red-fg</code>.</CardSubtitle>
<div class="d-flex flex-wrap gap-3">
{
colors.map((color) => (
<div class="img-labeled">
<img src={photo('a-visit-to-the-bookstore.jpg')} class="rounded" width="110" alt="A visit to the bookstore" />
<ImageLabel color={color}>New</ImageLabel>
</div>
))
}
</div>
</CardBody>
</Card>
</div>
<div class="col-md-6">
<Card>
<CardBody>
<CardTitle>Position</CardTitle>
<CardSubtitle>Move the label to another corner with <code>.img-label-top-start</code>, <code>.img-label-top-end</code> or <code>.img-label-bottom-start</code>.</CardSubtitle>
<div class="d-flex flex-wrap gap-3">
{
positions.map((item) => (
<div class="img-labeled">
<img src={photo('a-woman-works-on-a-laptop-at-home.jpg')} class="rounded" width="140" alt="A woman working on a laptop" />
<ImageLabel position={item.position}>{item.label}</ImageLabel>
</div>
))
}
</div>
</CardBody>
</Card>
</div>
<div class="col-md-6">
<Card>
<CardBody>
<CardTitle>Multiple labels</CardTitle>
<CardSubtitle>Put several labels on one image, each in its own corner.</CardSubtitle>
<div class="img-labeled">
<img src={photo('brainstorming-session-with-creative-designers-2.jpg')} class="rounded" width="320" alt="Designers at a brainstorming session" />
<ImageLabel position="top-start" color="red">Live</ImageLabel>
<ImageLabel position="top-end"><Icon name="lock" /> Members</ImageLabel>
<ImageLabel>4K</ImageLabel>
</div>
</CardBody>
</Card>
</div>
<div class="col-12">
<Card>
<CardBody>
<CardTitle>In a list</CardTitle>
<CardSubtitle>Labels mark single items in a list of products or media.</CardSubtitle>
<div class="row g-3">
{
products.map((product) => (
<div class="col-md-4">
<div class="row g-3 align-items-center">
<a href="#" class="col-auto">
<span class="img-labeled">
<img src={photo(product.file)} class="rounded" width="120" alt={product.name} />
<ImageLabel color={product.color}>{product.label}</ImageLabel>
</span>
</a>
<div class="col">
<div class="fw-medium">{product.name}</div>
<div class="text-secondary">{product.price}</div>
</div>
</div>
</div>
))
}
</div>
</CardBody>
</Card>
</div>
</div>
</DefaultLayout>
@@ -0,0 +1,45 @@
---
// Image labels in context: a card with a short product list, the way a shop or
// a media library would use them. Each thumbnail carries one label - "AI" for a
// generated picture, "New" and a red discount.
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
import Card from '@ui/Card.astro'
import CardHeader from '@ui/CardHeader.astro'
import CardTitle from '@ui/CardTitle.astro'
import ImageLabel from '@ui/ImageLabel.astro'
const products = [
{ name: 'Morning coffee set', category: 'Kitchen', price: '$49', file: 'cup-of-coffee-and-an-open-book.jpg', label: 'AI', color: undefined },
{ name: 'Designer sofa', category: 'Living room', price: '$849', file: 'blue-sofa-with-pillows-in-a-designer-living-room-interior.jpg', label: 'New', color: 'primary' },
{ name: 'Bedside stool', category: 'Bedroom', price: '$59', file: 'books-and-purple-flowers-on-a-wooden-stool-by-the-bed.jpg', label: '-20%', color: 'red' },
]
---
<ScreenshotLayout title="Image label in a product list" columns={1.5}>
<Card>
<CardHeader>
<CardTitle>Products</CardTitle>
</CardHeader>
<div class="list-group list-group-flush">
{
products.map((product) => (
<div class="list-group-item">
<div class="row align-items-center">
<div class="col-auto">
<div class="img-labeled">
<img src={`/static/photos/${product.file}`} class="rounded object-fit-cover" width="128" height="96" alt="" />
<ImageLabel color={product.color}>{product.label}</ImageLabel>
</div>
</div>
<div class="col">
<div class="fw-medium">{product.name}</div>
<div class="text-secondary">{product.category}</div>
</div>
<div class="col-auto fw-medium">{product.price}</div>
</div>
</div>
))
}
</div>
</Card>
</ScreenshotLayout>
+40
View File
@@ -0,0 +1,40 @@
---
// Image label on its own, no card: four photos in a grid, each showing a
// different use of the label - a plain text, an icon with a text, a colored
// label in another corner and a square icon-only label. Enlarged with the
// --tblr-img-label-* variables so the labels read at picture size.
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
import Icon from '@ui/Icon.astro'
import ImageLabel from '@ui/ImageLabel.astro'
const labelStyle = '--tblr-img-label-font-size: 1rem; --tblr-img-label-icon-size: 1rem; --tblr-img-label-offset: 0.75rem; --tblr-img-label-padding-y: 0.375rem; --tblr-img-label-padding-x: 0.625rem; --tblr-img-label-border-radius: var(--tblr-border-radius)'
---
<ScreenshotLayout title="Image label" columns={2}>
<div class="row g-4">
<div class="col-6">
<div class="img-labeled d-block">
<img src="/static/photos/cup-of-coffee-and-an-open-book.jpg" class="rounded-3 w-100 object-fit-cover" style="aspect-ratio: 4 / 3" alt="" />
<ImageLabel style={labelStyle}>AI</ImageLabel>
</div>
</div>
<div class="col-6">
<div class="img-labeled d-block">
<img src="/static/photos/brainstorming-session-with-creative-designers.jpg" class="rounded-3 w-100 object-fit-cover" style="aspect-ratio: 4 / 3" alt="" />
<ImageLabel style={labelStyle}><Icon name="player-play" /> 1:24</ImageLabel>
</div>
</div>
<div class="col-6">
<div class="img-labeled d-block">
<img src="/static/photos/blue-sofa-with-pillows-in-a-designer-living-room-interior.jpg" class="rounded-3 w-100 object-fit-cover" style="aspect-ratio: 4 / 3" alt="" />
<ImageLabel position="top-end" icon style={labelStyle}><Icon name="crown" /></ImageLabel>
</div>
</div>
<div class="col-6">
<div class="img-labeled d-block">
<img src="/static/photos/people-by-a-banquet-table-full-with-food.jpg" class="rounded-3 w-100 object-fit-cover" style="aspect-ratio: 4 / 3" alt="" />
<ImageLabel position="top-start" color="red" style={labelStyle}>-20%</ImageLabel>
</div>
</div>
</div>
</ScreenshotLayout>
+4
View File
@@ -230,6 +230,10 @@
"title": "Icon",
"url": "/ui/components/icon"
},
{
"title": "Image label",
"url": "/ui/components/image-label"
},
{
"title": "Legend",
"url": "/ui/components/legend"
+5
View File
@@ -173,6 +173,11 @@
"title": "Hover gallery",
"badge": "New"
},
"image-labels": {
"url": "image-labels.html",
"title": "Image labels",
"badge": "New"
},
"legend": {
"url": "legend.html",
"title": "Legend"
+22
View File
@@ -0,0 +1,22 @@
---
// Image label (.img-label): a short label pinned to a corner of an image.
// Place it next to the image inside an `.img-labeled` wrapper.
interface Props {
/** corner of the image; core/scss/ui/_images.scss `.img-label-{position}`, bottom-end is the default */
position?: 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end'
/** background color utility `bg-{color}` with its matching `text-{color}-fg` */
color?: string | undefined
/** the label holds only an icon, so it becomes a square (`.img-label-icon`) */
icon?: boolean
class?: string
/** remaining attributes (id, data-*, aria-*, …) are forwarded to the root element */
[key: string]: unknown
}
const { position = 'bottom-end', color, icon, class: className, ...rest } = Astro.props
const classes = ['img-label', position !== 'bottom-end' && `img-label-${position}`, color && `bg-${color} text-${color}-fg`, icon && 'img-label-icon', className]
---
<span class:list={classes} {...rest}><slot /></span>