diff --git a/.build/generate-tokens.ts b/.build/generate-tokens.ts
index 1ca60cbd2..98e11dc94 100644
--- a/.build/generate-tokens.ts
+++ b/.build/generate-tokens.ts
@@ -30,6 +30,7 @@ const TOKENS = [
{ name: 'aspectRatios', type: 'AspectRatio', module: 'v', variable: '$aspect-ratios', kind: 'map', source: 'core/scss/_variables.scss' },
{ name: 'breakpoints', type: 'Breakpoint', module: 's', variable: '$grid-breakpoints', kind: 'map', source: 'core/scss/_settings.scss' },
{ name: 'breadcrumbVariants', type: 'BreadcrumbVariant', module: 'v', variable: '$breadcrumb-variants', kind: 'map', source: 'core/scss/_variables.scss' },
+ { name: 'gradients', type: 'Gradient', module: 'v', variable: '$gradients', kind: 'map', source: 'core/scss/_variables.scss' },
{ name: 'formValidationStates', type: 'FormValidationState', module: 'v', variable: '$form-validation-states', kind: 'map', source: 'core/scss/_variables.scss' },
{ name: 'paymentProviders', type: 'PaymentProvider', module: 'v', variable: '$payment-providers', kind: 'list', source: 'core/scss/_variables.scss' },
{ name: 'flagCountries', type: 'FlagCountry', module: 'v', variable: '$flag-countries', kind: 'list', source: 'core/scss/_variables.scss' },
diff --git a/.changeset/btn-outline-gradient.md b/.changeset/btn-outline-gradient.md
new file mode 100644
index 000000000..9771f2c93
--- /dev/null
+++ b/.changeset/btn-outline-gradient.md
@@ -0,0 +1,7 @@
+---
+"@tabler/core": minor
+"@tabler/preview": minor
+"@tabler/docs": minor
+---
+
+Added `.btn-outline-gradient` buttons with a gradient border, using the named gradients shared with `.card-gradient`.
diff --git a/core/scss/_variables.scss b/core/scss/_variables.scss
index caa133799..f32817f7c 100644
--- a/core/scss/_variables.scss
+++ b/core/scss/_variables.scss
@@ -1207,6 +1207,63 @@ $otp-slot-box-shadow: var(--shadow-input) !default;
$otp-separator-color: var(--secondary) !default;
// scss-docs-end otp-variables
+// Gradients
+// Multi-color presets shared by `.card-gradient-*` and `.btn-outline-gradient-*`.
+// Each value is a comma-separated list of color stops.
+// scss-docs-start gradients
+$gradients: (
+ 'rainbow': (
+ #78c5d6,
+ #459ba8,
+ #79c267,
+ #c5d647,
+ #f5d63d,
+ #f08b33,
+ #e868a2,
+ #be61a5,
+ ),
+ 'sun': (
+ #fd1d1d,
+ #fcb045,
+ ),
+ 'snow': (
+ #333,
+ #e9ecef,
+ ),
+ 'ocean': (
+ #1cb5e0,
+ #000851,
+ ),
+ 'mellow': (
+ #f8ff00,
+ #3ad59f,
+ ),
+ 'disco': (
+ #fc466b,
+ #3f5efb,
+ ),
+ 'psychedelic': (
+ #fcc5e4,
+ #fda34b,
+ #ff7882,
+ #c8699e,
+ #7046aa,
+ #0c1db8,
+ #020f75,
+ ),
+ 'love': (
+ #f235e6,
+ #bc0707,
+ ),
+ 'gold': (
+ #9d4100,
+ #bf7122,
+ #f59f00,
+ #ffd700,
+ ),
+) !default;
+// scss-docs-end gradients
+
// Cards
// scss-docs-start card-variables
$card-spacer-y: 1.25rem !default;
diff --git a/core/scss/tests/_buttons.test.scss b/core/scss/tests/_buttons.test.scss
new file mode 100644
index 000000000..854fc2f6a
--- /dev/null
+++ b/core/scss/tests/_buttons.test.scss
@@ -0,0 +1,23 @@
+// `.btn-outline-gradient-*` presets share one selector list with the base
+// class, so a preset works without `.btn-outline-gradient` next to it. The
+// stops come from the shared `$gradients` map, so a card and a button with the
+// same name show the same gradient.
+@use 'sass:list';
+@use 'sass:map';
+@use '../config' as *;
+@use '../ui/buttons' as buttons;
+
+@include describe('btn-outline-gradient') {
+ @include it('shares the named gradient stops with .card-gradient') {
+ @include assert-equal(map.get($gradients, 'sun'), (#fd1d1d, #fcb045));
+ @include assert-true(map.has-key($gradients, 'rainbow'));
+ }
+
+ @include it('lists the base class and every color and gradient preset') {
+ $selectors: buttons.btn-gradient-selectors();
+ @include assert-equal(list.nth($selectors, 1), '.btn-outline-gradient');
+ @include assert-true(list.index($selectors, '.btn-outline-gradient-primary') != null);
+ @include assert-true(list.index($selectors, '.btn-outline-gradient-rainbow') != null);
+ @include assert-equal(list.length($selectors), 1 + list.length(map.keys($theme-colors)) + list.length(map.keys($gradients)));
+ }
+}
diff --git a/core/scss/ui/_buttons.scss b/core/scss/ui/_buttons.scss
index c1f4bce13..698c5b441 100644
--- a/core/scss/ui/_buttons.scss
+++ b/core/scss/ui/_buttons.scss
@@ -175,6 +175,66 @@ $btn-colors: map.keys(map.merge($theme-colors, $social-colors));
--btn-box-shadow: none;
}
+// Gradient ring: a surface-colored layer clipped to the padding box sits on a
+// gradient layer that fills the border box, so only the border shows the
+// gradient — the same trick as `.card-gradient`. Presets share the `.btn-outline-gradient`
+// base through one selector list, so a preset class works on its own.
+$btn-gradient-names: list.join(map.keys($theme-colors), map.keys($gradients));
+
+@function btn-gradient-selectors() {
+ $selectors: '.btn-outline-gradient';
+ @each $name in $btn-gradient-names {
+ $selectors: list.append($selectors, '.btn-outline-gradient-#{$name}', $separator: comma);
+ }
+ @return $selectors;
+}
+
+$btn-gradient-selectors: btn-gradient-selectors();
+
+#{$btn-gradient-selectors} {
+ // scss-docs-start btn-gradient-css-vars
+ --btn-gradient: var(--primary), var(--primary);
+ --btn-gradient-direction: 135deg;
+ // scss-docs-end btn-gradient-css-vars
+ --btn-border-color: transparent;
+ --btn-hover-border-color: transparent;
+ --btn-active-border-color: transparent;
+ --btn-disabled-border-color: transparent;
+ --btn-box-shadow: none;
+ // The hover and active fills are translucent tints, so a solid surface layer
+ // sits under them — otherwise the gradient would bleed through on hover.
+ background:
+ linear-gradient(var(--btn-bg), var(--btn-bg)) padding-box,
+ linear-gradient(var(--bg-surface), var(--bg-surface)) padding-box,
+ linear-gradient(var(--btn-gradient-direction), var(--btn-gradient)) border-box;
+}
+
+// `.btn:hover` only sets `background-color`, which sits under the two opaque
+// image layers, so the hover and active fills go through the token instead.
+// `:is()` keeps the state rules to one selector list each.
+:is(#{$btn-gradient-selectors}):hover,
+.btn-check + :is(#{$btn-gradient-selectors}):hover {
+ --btn-bg: var(--btn-hover-bg);
+}
+
+:is(#{$btn-gradient-selectors}):active,
+:is(#{$btn-gradient-selectors}).active,
+.btn-check:checked + :is(#{$btn-gradient-selectors}) {
+ --btn-bg: var(--btn-active-bg);
+}
+
+@each $color in map.keys($theme-colors) {
+ .btn-outline-gradient-#{$color} {
+ --btn-gradient: var(--#{$color}), var(--#{$color}-darken);
+ }
+}
+
+@each $name, $stops in $gradients {
+ .btn-outline-gradient-#{$name} {
+ --btn-gradient: #{$stops};
+ }
+}
+
//
// Button sizes
//
diff --git a/core/scss/ui/_cards.scss b/core/scss/ui/_cards.scss
index 13eebfbed..6acb3ab4c 100644
--- a/core/scss/ui/_cards.scss
+++ b/core/scss/ui/_cards.scss
@@ -734,40 +734,10 @@ Card gradient
}
}
-.card-gradient-rainbow {
- --card-gradient: #78c5d6, #459ba8, #79c267, #c5d647, #f5d63d, #f08b33, #e868a2, #be61a5;
-}
-
-.card-gradient-sun {
- --card-gradient: #fd1d1d, #fcb045;
-}
-
-.card-gradient-snow {
- --card-gradient: #333, #e9ecef;
-}
-
-.card-gradient-ocean {
- --card-gradient: #1cb5e0, #000851;
-}
-
-.card-gradient-mellow {
- --card-gradient: #f8ff00, #3ad59f;
-}
-
-.card-gradient-disco {
- --card-gradient: #fc466b, #3f5efb;
-}
-
-.card-gradient-psychedelic {
- --card-gradient: #fcc5e4, #fda34b, #ff7882, #c8699e, #7046aa, #0c1db8, #020f75;
-}
-
-.card-gradient-love {
- --card-gradient: #f235e6, #bc0707;
-}
-
-.card-gradient-gold {
- --card-gradient: #9d4100, #bf7122, #f59f00, #ffd700;
+@each $name, $stops in $gradients {
+ .card-gradient-#{$name} {
+ --card-gradient: #{$stops};
+ }
}
.card-gradient-animated {
diff --git a/docs/content/ui/components/button.mdx b/docs/content/ui/components/button.mdx
index e81f68a72..2b4bf41ff 100644
--- a/docs/content/ui/components/button.mdx
+++ b/docs/content/ui/components/button.mdx
@@ -25,6 +25,8 @@ classnames:
style:
- class: btn-outline
desc: Transparent fill with a colored border
+ - class: btn-outline-gradient
+ desc: Gradient ring on the border; btn-outline-gradient-{color} and the named gradients change the colors
- class: btn-ghost
desc: No fill and no border until hovered
- class: btn-link
@@ -187,6 +189,30 @@ The `.btn-outline-*` classes keep the color on the border and the text and leave
+## Outline gradient buttons
+
+`.btn-outline-gradient` keeps the fill empty, like an outline button, but draws the border as a gradient. Add a theme color, such as `.btn-outline-gradient-purple`, or one of the named gradients shared with [card gradients](/ui/components/card-gradient). The fill still follows the theme, so the ring works in dark mode without extra classes.
+
+
.card-gradient.