From c4934364a0cf388c14be47aacc8f794fcb3d7716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kuna?= <1282324+codecalm@users.noreply.github.com> Date: Mon, 19 Oct 2026 18:34:40 +0200 Subject: [PATCH] Add Icon list component (#3274) --- .changeset/icon-list.md | 7 + core/scss/_variables.scss | 9 + core/scss/ui/_lists.scss | 39 ++ docs/content/ui/components/icon-list.mdx | 174 +++++++++ preview/pages/icon-lists.astro | 337 ++++++++++++++++++ preview/pages/license.astro | 28 +- preview/pages/trial-ended.astro | 28 +- screenshots/pages/icon-list.astro | 77 ++++ shared/components/cards/PricingCard.astro | 25 +- shared/components/cards/PricingPlan.astro | 33 +- shared/components/cards/UserInfo.astro | 47 ++- .../marketing/sections/Pricing.astro | 56 +-- .../marketing/sections/PricingBanner.astro | 22 +- shared/data/docs.json | 4 + shared/data/menu.json | 5 + shared/ui/IconList.astro | 21 ++ shared/ui/IconListItem.astro | 19 + 17 files changed, 792 insertions(+), 139 deletions(-) create mode 100644 .changeset/icon-list.md create mode 100644 docs/content/ui/components/icon-list.mdx create mode 100644 preview/pages/icon-lists.astro create mode 100644 screenshots/pages/icon-list.astro create mode 100644 shared/ui/IconList.astro create mode 100644 shared/ui/IconListItem.astro diff --git a/.changeset/icon-list.md b/.changeset/icon-list.md new file mode 100644 index 000000000..a3fa72a11 --- /dev/null +++ b/.changeset/icon-list.md @@ -0,0 +1,7 @@ +--- +"@tabler/core": minor +"@tabler/preview": minor +"@tabler/docs": minor +--- + +Added the icon list component: `.list-icon` turns a plain list into a list with icon markers, such as a checklist of features, with `.list-icon-inline` for entries in a row. diff --git a/core/scss/_variables.scss b/core/scss/_variables.scss index dc44b7357..47a9a2205 100644 --- a/core/scss/_variables.scss +++ b/core/scss/_variables.scss @@ -2043,6 +2043,15 @@ $list-group-item-gap-lg: 1.25rem !default; $list-group-variant-border-alpha: 0.24 !default; $list-group-variant-hover-alpha: 0.16 !default; +// Icon list +// scss-docs-start list-icon-variables +$list-icon-gap: 0.5rem !default; +$list-icon-row-gap: 0.5rem !default; +$list-icon-inline-gap: 1.5rem !default; +$list-icon-size: 1rem !default; +$list-icon-color: var(--secondary) !default; +// scss-docs-end list-icon-variables + // Forms $input-disabled-bg: var(--bg-forms-disabled) !default; $input-border-color: var(--border-color) !default; diff --git a/core/scss/ui/_lists.scss b/core/scss/ui/_lists.scss index e7ec6f6ba..351e563e5 100644 --- a/core/scss/ui/_lists.scss +++ b/core/scss/ui/_lists.scss @@ -204,3 +204,42 @@ content: ' · '; } } + +// Icon list: a list whose markers are icons instead of bullets, such as a +// checklist of features. The first `.icon` in an entry is the marker. +.list-icon { + // scss-docs-start list-icon-css-vars + --list-icon-gap: #{$list-icon-gap}; + --list-icon-row-gap: #{$list-icon-row-gap}; + --list-icon-inline-gap: #{$list-icon-inline-gap}; + --list-icon-size: #{$list-icon-size}; + --list-icon-color: #{$list-icon-color}; + // scss-docs-end list-icon-css-vars + display: flex; + flex-direction: column; + gap: var(--list-icon-row-gap); + padding-inline-start: 0; + list-style: none; + + > * { + display: flex; + gap: var(--list-icon-gap); + align-items: flex-start; + } + + > * > .icon:first-child { + --icon-size: var(--list-icon-size); + flex-shrink: 0; + // center the marker on the first line of text: `.icon` sets its own font + // size, which would make `1lh` the icon's line height, so inherit the text's + margin-top: calc((1lh - var(--list-icon-size)) / 2); + font-size: inherit; + color: var(--list-icon-color); + } +} + +// Entries in a row that wraps, for a short list of highlights +.list-icon-inline { + flex-flow: row wrap; + gap: var(--list-icon-row-gap) var(--list-icon-inline-gap); +} diff --git a/docs/content/ui/components/icon-list.mdx b/docs/content/ui/components/icon-list.mdx new file mode 100644 index 000000000..dc485ad03 --- /dev/null +++ b/docs/content/ui/components/icon-list.mdx @@ -0,0 +1,174 @@ +--- +title: Icon list +summary: An icon list replaces the bullets of a list with icons, such as a check mark in a list of features, a plan's benefits or the steps of a setup. +description: Show a list with icon markers instead of bullets. Change the icon and the color per list or per entry, or lay the entries out in a row. +related: [/ui/components/list-group, /ui/components/icon] +source: core/scss/ui/_lists.scss +css-vars: list-icon +classnames: + component: + - class: list-icon + desc: The list, on a ul, ol or div + part: + - class: icon + desc: The first icon in an entry becomes its marker + direction: + - class: list-icon-inline + desc: Lays the entries out in a row that wraps + color: + - class: text-{color} + desc: Colors the marker of one entry, for example text-green on the icon +--- +import Example from '@components/Example.astro'; +import Icon from '@ui/Icon.astro'; + +## Default markup + +Add `.list-icon` to a list and start each entry with an icon. The first icon in an entry is the marker: it sits in front of the text, centered on the first line, in the secondary color. + + + + + +## Color + +Color all markers at once with the `--tblr-list-icon-color` variable on the list. A `text-{color}` utility on one icon colors that marker only. + + + + + +## Different icons + +Each entry can use its own icon. Combine icons and colors to show a status, for example the steps of a setup. + + + + + +## Included and excluded + +In a pricing table, use a check for what the plan has and a muted cross for what it lacks. Mute the whole entry with `.text-secondary` so the text reads as unavailable too. + + + + + +## Inline + +Add `.list-icon-inline` to show short entries in a row that wraps, for example the highlights under a product name. + + + + + +## Long entries + +When an entry wraps to more lines, the marker stays on the first line. The marker follows the line height of the list, so it also fits small or large text. + + + + + +## Pros and cons + +Put two lists side by side to compare: green checks on one side, red crosses on the other. + + +
+
+
    +
  • Fast to set up
  • +
  • No vendor lock-in
  • +
  • Great documentation
  • +
+
+
+
    +
  • No built-in auth
  • +
  • Smaller community
  • +
+
+
+
+ +## Entries with a description + +An entry can hold more than one line: wrap the content in an element with a title and a muted description. The marker stays next to the title. Raise `--tblr-list-icon-row-gap` to give the entries more room. + + + + + +## Links + +The marker can describe the entry instead of ticking it off, for example in a list of contact details with links. + + + + + +## Custom look + +Set the `--tblr-list-icon-*` variables on the list to change the size of the markers and the gaps between the entries. + + + + + +## Accessibility + +- Use a real list element, `ul` or `ol`, so screen readers announce the number of entries and let the user jump between them. +- The icons are decorative, so screen readers skip them. When the icon carries meaning, for example a check against a cross, add the meaning as a `.visually-hidden` text, such as "Included" or "Not included". diff --git a/preview/pages/icon-lists.astro b/preview/pages/icon-lists.astro new file mode 100644 index 000000000..bc36a1260 --- /dev/null +++ b/preview/pages/icon-lists.astro @@ -0,0 +1,337 @@ +--- +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 IconList from '@ui/IconList.astro' +import IconListItem from '@ui/IconListItem.astro' + +const features = ['Unlimited projects', 'Priority support', 'Custom domains', 'Advanced analytics'] + +const layouts = ['Boxed', 'Combined', 'Condensed', 'Fluid', 'Horizontal', 'Vertical', 'RTL mode', 'Dark mode'] + +const plans = [ + { name: 'Free', price: '$0', included: 2 }, + { name: 'Pro', price: '$29', included: 4 }, +] + +const overview = [ + ['Boxed', 'Combined', 'Condensed', 'Fluid', 'Horizontal', 'Vertical'], + ['Dark mode', 'RTL mode', 'Navbar sticky', 'Navbar overlap', 'Folded sidebar', 'Auto color mode'], +] + +const pros = ['Fast to set up', 'No vendor lock-in', 'Great documentation'] +const cons = ['No built-in auth', 'Smaller community'] + +const requirements = [ + { text: 'At least 8 characters', met: true }, + { text: 'One upper case letter', met: true }, + { text: 'One number', met: false }, + { text: 'One special character', met: false }, +] + +const changes = [ + { icon: 'plus', color: 'green', text: 'Added the icon list component' }, + { icon: 'arrow-up', color: 'blue', text: 'Faster rendering of large tables' }, + { icon: 'bug', color: 'red', text: 'Fixed the dropdown position in RTL mode' }, + { icon: 'trash', color: 'secondary', text: 'Removed the old inline list dots' }, +] + +const contact = [ + { icon: 'mail', text: 'hello@tabler.io', href: 'mailto:hello@tabler.io' }, + { icon: 'phone', text: '+1 (555) 123-4567', href: 'tel:+15551234567' }, + { icon: 'map-pin', text: '1 Market Street, San Francisco' }, + { icon: 'world', text: 'tabler.io', href: 'https://tabler.io' }, +] + +const details = [ + { title: 'Unlimited projects', text: 'Create as many projects as you need, with no extra charge.' }, + { title: 'Priority support', text: 'Your questions go to the top of the queue.' }, + { title: 'Custom domains', text: 'Serve every project from your own domain with a free certificate.' }, +] + +const steps = [ + { icon: 'circle-check', color: 'green', text: 'Account created' }, + { icon: 'circle-check', color: 'green', text: 'Email confirmed' }, + { icon: 'progress', color: 'blue', text: 'Profile in progress' }, + { icon: 'circle-dashed', color: 'secondary', text: 'Add a payment method' }, +] +--- + + + + +
+
+ + + Basic + Add .list-icon to a list. The first icon in each entry is the marker. + + {features.map((text) => {text})} + + + +
+ +
+ + + Color + + Color all markers with the --tblr-list-icon-color variable, or one marker with a text-* utility. + + + {features.map((text) => {text})} + + + +
+ +
+ + + Different icons + Each entry can use its own icon and color, for example a status. + + { + steps.map((step) => ( + + {step.text} + + )) + } + + + +
+ +
+ + + Included and excluded + A check for what the plan has and a muted cross for what it lacks. + + { + features.map((text, i) => + i < 2 ? ( + {text} + ) : ( + + {text} + + ), + ) + } + + + +
+ +
+ + + Inline + + Add .list-icon-inline to show short entries in a row that wraps. + + + {layouts.map((text) => {text})} + + + +
+ +
+ + + Long entries + The marker stays on the first line when an entry wraps. + + Two-factor authentication keeps the account safe even when the password leaks. + Backups run every night and are kept for thirty days in a separate region. + Support answers within one business day, in English and Polish. + + + +
+ +
+ + + Small text + The marker follows the line height of the list, so it also fits small or large text. + + {features.map((text) => {text})} + + + +
+ +
+ + + Pros and cons + Two lists side by side, green checks against red crosses. +
+
+ + {pros.map((text) => {text})} + +
+
+ + {cons.map((text) => {text})} + +
+
+
+
+
+ +
+ + + Password requirements + A checklist under a form field, each rule marked as met or not. +
+ + +
+ + { + requirements.map((rule) => + rule.met ? ( + + {rule.text} + + ) : ( + + {rule.text} + + ), + ) + } + +
+
+
+ +
+ + + Release notes + An icon per kind of change: added, improved, fixed, removed. + + { + changes.map((change) => ( + + {change.text} + + )) + } + + + +
+ +
+ + + Contact details + Icons as markers for a list of links. + + {contact.map((row) => {row.href ? {row.text} : row.text})} + + + +
+ +
+ + + Entries with a description + A title and a muted second line in each entry; the marker stays on the title. + + { + details.map((row) => ( + +
+
{row.title}
+
{row.text}
+
+
+ )) + } +
+
+
+
+ +
+ + + Large text + Raise the font size and the marker size together for a landing page. + + {features.slice(0, 3).map((text) => {text})} + + + +
+ +
+ + + Feature overview + A long list split into columns, as on a comparison or a landing page. +
+ { + overview.map((column) => ( +
+ + {column.map((text) => ( + {text} + ))} + +
+ )) + } +
+
+
+
+ + { + plans.map((plan) => ( +
+ + + {plan.name} +
+ {plan.price} + /mo +
+ + {features.map((text, i) => + i < plan.included ? ( + {text} + ) : ( + + {text} + + ), + )} + + + Choose {plan.name} + +
+
+
+ )) + } +
+
diff --git a/preview/pages/license.astro b/preview/pages/license.astro index 279835a2a..0cd0f513e 100644 --- a/preview/pages/license.astro +++ b/preview/pages/license.astro @@ -1,6 +1,8 @@ --- import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import Icon from '@ui/Icon.astro' +import IconList from '@ui/IconList.astro' +import IconListItem from '@ui/IconListItem.astro' import Card from '@ui/Card.astro' import CardBody from '@ui/CardBody.astro' import CardFooter from '@ui/CardFooter.astro' @@ -62,23 +64,23 @@ const licenseHtml = `

Permissions

- + + Commercial use + Modification + Distribution + Private use +

Limitations

- + + Liability + Warranty +

Conditions

- + + License and copyright notice + This is not legal advice. diff --git a/preview/pages/trial-ended.astro b/preview/pages/trial-ended.astro index 56716878d..2aba7084d 100644 --- a/preview/pages/trial-ended.astro +++ b/preview/pages/trial-ended.astro @@ -1,6 +1,7 @@ --- import SingleLayout from '@shared/layouts/SingleLayout.astro' -import Icon from '@ui/Icon.astro' +import IconList from '@ui/IconList.astro' +import IconListItem from '@ui/IconListItem.astro' --- @@ -10,29 +11,26 @@ import Icon from '@ui/Icon.astro'

If you want to continue to benefit from Tabler, it's time to upgrade your plan.

- + +
Upgrade to a paid plan diff --git a/screenshots/pages/icon-list.astro b/screenshots/pages/icon-list.astro new file mode 100644 index 000000000..93d8197a9 --- /dev/null +++ b/screenshots/pages/icon-list.astro @@ -0,0 +1,77 @@ +--- +// Icon list in context: two cards side by side. On the left a pricing plan +// with green checks for what is included and muted crosses for what is not; +// on the right a setup checklist where every entry has its own icon, color +// and a short description. Both show the marker centered on the first line. +import ScreenshotLayout from '../layouts/ScreenshotLayout.astro' +import Card from '@ui/Card.astro' +import CardBody from '@ui/CardBody.astro' +import CardTitle from '@ui/CardTitle.astro' +import IconList from '@ui/IconList.astro' +import IconListItem from '@ui/IconListItem.astro' + +const plan = [ + { text: 'Unlimited projects', included: true }, + { text: 'Up to 10 team members', included: true }, + { text: 'Custom domains', included: true }, + { text: 'Priority support', included: true }, + { text: 'Advanced analytics', included: false }, + { text: 'Single sign-on', included: false }, +] + +const checklist = [ + { icon: 'circle-check', color: 'green', title: 'Account created', text: 'Your workspace is ready to use.' }, + { icon: 'circle-check', color: 'green', title: 'Email confirmed', text: 'We sent a link to your inbox.' }, + { icon: 'progress', color: 'blue', title: 'Team invited', text: '2 of 5 invitations accepted.' }, + { icon: 'circle-dashed', color: 'secondary', title: 'Add a payment method', text: 'Needed before the trial ends.' }, +] +--- + + +
+
+ + +
Pro plan
+
+
$29
+
/ month
+
+ + { + plan.map((row) => + row.included ? ( + {row.text} + ) : ( + + {row.text} + + ), + ) + } + + Choose Pro +
+
+
+
+ + + Getting started + + { + checklist.map((step) => ( + + + {step.title} + {step.text} + + + )) + } + + + +
+
+
diff --git a/shared/components/cards/PricingCard.astro b/shared/components/cards/PricingCard.astro index 9616edd2e..cbd11f93d 100644 --- a/shared/components/cards/PricingCard.astro +++ b/shared/components/cards/PricingCard.astro @@ -1,6 +1,8 @@ --- // Feature names are a fixed list; the `features` string is split per-character into a 1/0 availability mask. import Icon from '@ui/Icon.astro' +import IconList from '@ui/IconList.astro' +import IconListItem from '@ui/IconListItem.astro' interface Props { price?: string @@ -31,17 +33,22 @@ const availableFeatures = features.split('')
${price}
-
    -
  • {users} Users
  • + + + {users} Users + { - featureNames.map((feature, i) => ( -
  • - {availableFeatures[i] === '1' ? : } - {feature} -
  • - )) + featureNames.map((feature, i) => + availableFeatures[i] === '1' ? ( + {feature} + ) : ( + + {feature} + + ), + ) } -
+
Choose plan diff --git a/shared/components/cards/PricingPlan.astro b/shared/components/cards/PricingPlan.astro index 20bf4c2ed..b2e76cd89 100644 --- a/shared/components/cards/PricingPlan.astro +++ b/shared/components/cards/PricingPlan.astro @@ -1,6 +1,7 @@ --- -import Icon from '@ui/Icon.astro' import Button from '@ui/Button.astro' +import IconList from '@ui/IconList.astro' +import IconListItem from '@ui/IconListItem.astro' ---
@@ -11,24 +12,18 @@ import Button from '@ui/Button.astro'
$199
/ month
-
    -
  • -
    -
    Unlimited Target Audience
    -
  • -
  • -
    -
    1 User Account
    -
  • -
  • -
    -
    100+ Video Tuts
    -
  • -
  • -
    -
    Public Displays
    -
  • -
+ + + Unlimited Target Audience + + + 1 User Account + + + 100+ Video Tuts + + Public Displays +

This plan is perfect for small businesses and startups. It includes all the features you need to get started.

-
    -
  • - Up to 5 developers -
  • -
  • - All marketing + app components -
  • -
  • - Figma UI Kit -
  • -
  • - Lifetime free updates -
  • -
+ + {features.map((text) => {text})} +
- + + {features.map((text) => {text})} +
@@ -73,20 +54,9 @@ const sectionClass = ['section', className]
per year
- + + {features.map((text) => {text})} + diff --git a/shared/components/marketing/sections/PricingBanner.astro b/shared/components/marketing/sections/PricingBanner.astro index 5a192e7a6..f663070a0 100644 --- a/shared/components/marketing/sections/PricingBanner.astro +++ b/shared/components/marketing/sections/PricingBanner.astro @@ -1,7 +1,10 @@ --- -import Icon from '@ui/Icon.astro' +import IconList from '@ui/IconList.astro' +import IconListItem from '@ui/IconListItem.astro' const sectionClass = ['section'] + +const features = ['Up to 5 developers', 'All marketing + app components', 'Figma UI Kit', 'Lifetime free updates'] ---
@@ -13,20 +16,9 @@ const sectionClass = ['section']

Get Marketing + Application UI for you and your team

-
    -
  • - Up to 5 developers -
  • -
  • - All marketing + app components -
  • -
  • - Figma UI Kit -
  • -
  • - Lifetime free updates -
  • -
+ + {features.map((text) => {text})} +
diff --git a/shared/data/docs.json b/shared/data/docs.json index 6d6129331..b258e4036 100644 --- a/shared/data/docs.json +++ b/shared/data/docs.json @@ -234,6 +234,10 @@ "title": "Icon", "url": "/ui/components/icon" }, + { + "title": "Icon list", + "url": "/ui/components/icon-list" + }, { "title": "Image label", "url": "/ui/components/image-label" diff --git a/shared/data/menu.json b/shared/data/menu.json index 2a3663002..6c339fd18 100644 --- a/shared/data/menu.json +++ b/shared/data/menu.json @@ -173,6 +173,11 @@ "title": "Hover gallery", "badge": "New" }, + "icon-lists": { + "url": "icon-lists.html", + "title": "Icon lists", + "badge": "New" + }, "image-labels": { "url": "image-labels.html", "title": "Image labels", diff --git a/shared/ui/IconList.astro b/shared/ui/IconList.astro new file mode 100644 index 000000000..4f8ed9eb2 --- /dev/null +++ b/shared/ui/IconList.astro @@ -0,0 +1,21 @@ +--- +// Icon list (.list-icon): a list whose markers are icons instead of bullets. +// Fill it with `IconListItem` entries. + +interface Props { + /** entries in a row that wraps (`.list-icon-inline`) */ + inline?: boolean + /** marker color for the whole list, any base color name, for example `green` */ + color?: string | undefined + class?: string + /** remaining attributes (id, data-*, aria-*, …) are forwarded to the root element */ + [key: string]: unknown +} + +const { inline, color, class: className, style, ...rest } = Astro.props + +const colorStyle = color ? `--tblr-list-icon-color: var(--tblr-${color})` : undefined +const styles = [colorStyle, style].filter(Boolean).join('; ') || undefined +--- + +
diff --git a/shared/ui/IconListItem.astro b/shared/ui/IconListItem.astro new file mode 100644 index 000000000..77340379f --- /dev/null +++ b/shared/ui/IconListItem.astro @@ -0,0 +1,19 @@ +--- +// Icon list entry: an icon marker followed by the text. Place inside `IconList`. +import Icon from '@ui/Icon.astro' + +interface Props { + /** Tabler icon name of the marker */ + icon?: string + /** filled icon variant */ + filled?: boolean | undefined + /** marker color of this entry only, `text-{color}` on the icon */ + color?: string | undefined + class?: string + [key: string]: unknown +} + +const { icon = 'check', filled, color, class: className, ...rest } = Astro.props +--- + +