From 681b6eb671ab438b75fc138d92a0d5d0f0ede010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kuna?= <1282324+codecalm@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:24:15 +0200 Subject: [PATCH] Add Image label component (#3267) --- .changeset/image-label.md | 7 + core/scss/_variables.scss | 14 ++ core/scss/ui/_images.scss | 61 ++++++++ docs/content/ui/components/image-label.mdx | 134 +++++++++++++++++ preview/pages/image-labels.astro | 148 +++++++++++++++++++ screenshots/pages/image-label-products.astro | 45 ++++++ screenshots/pages/image-label.astro | 40 +++++ shared/data/docs.json | 4 + shared/data/menu.json | 5 + shared/ui/ImageLabel.astro | 22 +++ 10 files changed, 480 insertions(+) create mode 100644 .changeset/image-label.md create mode 100644 docs/content/ui/components/image-label.mdx create mode 100644 preview/pages/image-labels.astro create mode 100644 screenshots/pages/image-label-products.astro create mode 100644 screenshots/pages/image-label.astro create mode 100644 shared/ui/ImageLabel.astro diff --git a/.changeset/image-label.md b/.changeset/image-label.md new file mode 100644 index 000000000..8893d3f0b --- /dev/null +++ b/.changeset/image-label.md @@ -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. diff --git a/core/scss/_variables.scss b/core/scss/_variables.scss index dd97a60f7..4f976e97d 100644 --- a/core/scss/_variables.scss +++ b/core/scss/_variables.scss @@ -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; diff --git a/core/scss/ui/_images.scss b/core/scss/ui/_images.scss index 4a62d2b5d..a376acd00 100644 --- a/core/scss/ui/_images.scss +++ b/core/scss/ui/_images.scss @@ -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; +} diff --git a/docs/content/ui/components/image-label.mdx b/docs/content/ui/components/image-label.mdx new file mode 100644 index 000000000..1db0afc6a --- /dev/null +++ b/docs/content/ui/components/image-label.mdx @@ -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. + + +
+ A book on the grass + AI +
+
+ +## 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. + + +
+
+ A woman working on a laptop + Top start +
+
+ A woman working on a laptop + Top end +
+
+ A woman working on a laptop + Bottom start +
+
+ A woman working on a laptop + Bottom end +
+
+
+ +## 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. + + +
+
+ A brainstorming session + 1:24 +
+
+ A woman using a laptop + Premium +
+
+
+ +## Colors + +Change the color with a background utility and its matching text color, for example `.bg-red` with `.text-red-fg`. + + +
+
+ A visit to the bookstore + New +
+
+ A visit to the bookstore + -20% +
+
+ A visit to the bookstore + Free +
+
+
+ +## Multiple labels + +Put several labels on one image, each in its own corner. + + +
+ Designers at a brainstorming session + Live + Members + 4K +
+
+ +## 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. + + +
+ A book on the grass + Sponsored +
+
+ +## 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. diff --git a/preview/pages/image-labels.astro b/preview/pages/image-labels.astro new file mode 100644 index 000000000..0a8806d28 --- /dev/null +++ b/preview/pages/image-labels.astro @@ -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' }, +] +--- + + + + +
+
+ + + Basic + Wrap the image in .img-labeled and add an .img-label next to it. +
+ A book on the grass + AI +
+
+
+
+ +
+ + + With an icon + A label can hold an icon, a text, or both. Add .img-label-icon to a label with only an icon. +
+
+ A brainstorming session + 1:24 +
+
+ A woman using a laptop + Premium +
+
+
+
+
+ +
+ + + Colors + Add a background utility such as .bg-red with its .text-red-fg. +
+ { + colors.map((color) => ( +
+ A visit to the bookstore + New +
+ )) + } +
+
+
+
+ +
+ + + Position + Move the label to another corner with .img-label-top-start, .img-label-top-end or .img-label-bottom-start. +
+ { + positions.map((item) => ( +
+ A woman working on a laptop + {item.label} +
+ )) + } +
+
+
+
+ +
+ + + Multiple labels + Put several labels on one image, each in its own corner. +
+ Designers at a brainstorming session + Live + Members + 4K +
+
+
+
+ +
+ + + In a list + Labels mark single items in a list of products or media. +
+ { + products.map((product) => ( +
+
+ + + {product.name} + {product.label} + + +
+
{product.name}
+
{product.price}
+
+
+
+ )) + } +
+
+
+
+
+
diff --git a/screenshots/pages/image-label-products.astro b/screenshots/pages/image-label-products.astro new file mode 100644 index 000000000..3938b8e2d --- /dev/null +++ b/screenshots/pages/image-label-products.astro @@ -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' }, +] +--- + + + + + Products + +
+ { + products.map((product) => ( +
+
+
+
+ + {product.label} +
+
+
+
{product.name}
+
{product.category}
+
+
{product.price}
+
+
+ )) + } +
+
+
diff --git a/screenshots/pages/image-label.astro b/screenshots/pages/image-label.astro new file mode 100644 index 000000000..8a7062a08 --- /dev/null +++ b/screenshots/pages/image-label.astro @@ -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)' +--- + + +
+
+
+ + AI +
+
+
+
+ + 1:24 +
+
+
+
+ + +
+
+
+
+ + -20% +
+
+
+
diff --git a/shared/data/docs.json b/shared/data/docs.json index 352b3ad3a..db5b7057d 100644 --- a/shared/data/docs.json +++ b/shared/data/docs.json @@ -230,6 +230,10 @@ "title": "Icon", "url": "/ui/components/icon" }, + { + "title": "Image label", + "url": "/ui/components/image-label" + }, { "title": "Legend", "url": "/ui/components/legend" diff --git a/shared/data/menu.json b/shared/data/menu.json index 42ea8719a..e07975f72 100644 --- a/shared/data/menu.json +++ b/shared/data/menu.json @@ -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" diff --git a/shared/ui/ImageLabel.astro b/shared/ui/ImageLabel.astro new file mode 100644 index 000000000..293a1f134 --- /dev/null +++ b/shared/ui/ImageLabel.astro @@ -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] +--- + +