diff --git a/frontend/src/app/main/ui/ds.cljs b/frontend/src/app/main/ui/ds.cljs
index b15d52724f..59895744ad 100644
--- a/frontend/src/app/main/ui/ds.cljs
+++ b/frontend/src/app/main/ui/ds.cljs
@@ -6,6 +6,8 @@
(ns app.main.ui.ds
(:require
+ [app.main.ui.ds.buttons.button :refer [button*]]
+ [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]]
[app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg* raw-svg-list]]
[app.main.ui.ds.foundations.typography :refer [typography-list]]
@@ -16,8 +18,10 @@
(def default
"A export used for storybook"
- #js {:Heading heading*
+ #js {:Button button*
+ :Heading heading*
:Icon icon*
+ :IconButton icon-button*
:Loader loader*
:RawSvg raw-svg*
:Text text*
diff --git a/frontend/src/app/main/ui/ds/_borders.scss b/frontend/src/app/main/ui/ds/_borders.scss
new file mode 100644
index 0000000000..165ade57d1
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/_borders.scss
@@ -0,0 +1,10 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+//
+// Copyright (c) KALEIDOS INC
+
+@use "./utils.scss" as *;
+
+// TODO: create actual tokens once we have them from design
+$br-8: px2rem(8);
diff --git a/frontend/src/app/main/ui/ds/_sizes.scss b/frontend/src/app/main/ui/ds/_sizes.scss
new file mode 100644
index 0000000000..f27838b6af
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/_sizes.scss
@@ -0,0 +1,10 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+//
+// Copyright (c) KALEIDOS INC
+
+@use "./utils.scss" as *;
+
+// TODO: create actual tokens once we have them from design
+$sz-32: px2rem(32);
diff --git a/frontend/src/app/main/ui/ds/buttons/_buttons.scss b/frontend/src/app/main/ui/ds/buttons/_buttons.scss
new file mode 100644
index 0000000000..7d8c896ac9
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/buttons/_buttons.scss
@@ -0,0 +1,132 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+//
+// Copyright (c) KALEIDOS INC
+
+@use "../_borders.scss" as *;
+@use "../_sizes.scss" as *;
+@use "../utils.scss" as *;
+
+%base-button {
+ --button-bg-color: initial;
+ --button-fg-color: initial;
+ --button-hover-bg-color: initial;
+ --button-hover-fg-color: initial;
+ --button-active-bg-color: initial;
+ --button-disabled-bg-color: initial;
+ --button-disabled-fg-color: initial;
+ --button-border-color: var(--button-bg-color);
+ --button-focus-inner-ring-color: initial;
+ --button-focus-outer-ring-color: initial;
+
+ appearance: none;
+ height: $sz-32;
+ border: none;
+ border-radius: $br-8;
+
+ background: var(--button-bg-color);
+ color: var(--button-fg-color);
+ border: 1px solid var(--button-border-color);
+
+ &:hover {
+ --button-bg-color: var(--button-hover-bg-color);
+ --button-fg-color: var(--button-hover-fg-color);
+ }
+
+ &:active {
+ --button-bg-color: var(--button-active-bg-color);
+ }
+
+ &:focus-visible {
+ outline: var(--button-focus-inner-ring-color) solid #{px2rem(2)};
+ outline-offset: -#{px2rem(3)};
+ --button-border-color: var(--button-focus-outer-ring-color);
+ --button-fg-color: var(--button-focus-fg-color);
+ }
+
+ &:disabled {
+ --button-bg-color: var(--button-disabled-bg-color);
+ --button-fg-color: var(--button-disabled-fg-color);
+ }
+}
+
+%base-button-primary {
+ --button-bg-color: var(--color-accent-primary);
+ --button-fg-color: var(--color-background-secondary);
+
+ --button-hover-bg-color: var(--color-accent-tertiary);
+ --button-hover-fg-color: var(--color-background-secondary);
+
+ --button-active-bg-color: var(--color-accent-tertiary);
+
+ --button-disabled-bg-color: var(--color-accent-primary-muted);
+ --button-disabled-fg-color: var(--color-background-secondary);
+
+ --button-focus-bg-color: var(--color-accent-primary);
+ --button-focus-fg-color: var(--color-background-secondary);
+ --button-focus-inner-ring-color: var(--color-background-secondary);
+ --button-focus-outer-ring-color: var(--color-accent-primary);
+
+ &:active {
+ box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgba(0, 0, 0, 0.2);
+ }
+}
+
+%base-button-secondary {
+ --button-bg-color: var(--color-background-tertiary);
+ --button-fg-color: var(--color-foreground-secondary);
+
+ --button-hover-bg-color: var(--color-background-tertiary);
+ --button-hover-fg-color: var(--color-accent-primary);
+
+ --button-active-bg-color: var(--color-background-quaternary);
+
+ --button-disabled-bg-color: transparent;
+ --button-disabled-fg-color: var(--color-foreground-secondary);
+
+ --button-focus-bg-color: var(--color-background-tertiary);
+ --button-focus-fg-color: var(--color-foreground-primary);
+ --button-focus-inner-ring-color: var(--color-background-secondary);
+ --button-focus-outer-ring-color: var(--color-accent-primary);
+}
+
+%base-button-ghost {
+ --button-bg-color: transparent;
+ --button-fg-color: var(--color-foreground-secondary);
+
+ --button-hover-bg-color: var(--color-background-tertiary);
+ --button-hover-fg-color: var(--color-accent-primary);
+
+ --button-active-bg-color: var(--color-background-quaternary);
+
+ --button-disabled-bg-color: transparent;
+ --button-disabled-fg-color: var(--color-accent-primary-muted);
+
+ --button-focus-bg-color: transparent;
+ --button-focus-fg-color: var(--color-foreground-secondary);
+ --button-focus-inner-ring-color: transparent;
+ --button-focus-outer-ring-color: var(--color-accent-primary);
+}
+
+%base-button-destructive {
+ --button-bg-color: var(--color-accent-error);
+ --button-fg-color: var(--color-foreground-primary);
+
+ --button-hover-bg-color: var(--color-background-error);
+ --button-hover-fg-color: var(--color-foreground-primary);
+
+ --button-active-bg-color: var(--color-accent-error);
+
+ --button-disabled-bg-color: var(--color-background-error);
+ --button-disabled-fg-color: var(--color-accent-error);
+
+ --button-focus-bg-color: var(--color-accent-error);
+ --button-focus-fg-color: var(--color-foreground-primary);
+ --button-focus-inner-ring-color: var(--color-background-primary);
+ --button-focus-outer-ring-color: var(--color-accent-primary);
+
+ &:active {
+ box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgba(0, 0, 0, 0.2);
+ }
+}
diff --git a/frontend/src/app/main/ui/ds/buttons/button.cljs b/frontend/src/app/main/ui/ds/buttons/button.cljs
new file mode 100644
index 0000000000..8086758bbd
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/buttons/button.cljs
@@ -0,0 +1,30 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) KALEIDOS INC
+
+(ns app.main.ui.ds.buttons.button
+ (:require-macros
+ [app.common.data.macros :as dm]
+ [app.main.style :as stl])
+ (:require
+ [app.main.ui.ds.foundations.assets.icon :refer [icon*]]
+ [rumext.v2 :as mf]))
+
+(def button-variants (set '("primary" "secondary" "ghost" "destructive")))
+
+(mf/defc button*
+ {::mf/props :obj}
+ [{:keys [variant icon children class] :rest props}]
+ (assert (or (nil? variant) (contains? button-variants variant) "expected valid variant"))
+ (let [variant (or variant "primary")
+ class (dm/str class " " (stl/css-case :button true
+ :button-primary (= variant "primary")
+ :button-secondary (= variant "secondary")
+ :button-ghost (= variant "ghost")
+ :button-destructive (= variant "destructive")))
+ props (mf/spread-props props {:class class})]
+ [:> "button" props
+ (when icon [:> icon* {:id icon :size "m"}])
+ [:span {:class (stl/css :label-wrapper)} children]]))
\ No newline at end of file
diff --git a/frontend/src/app/main/ui/ds/buttons/button.scss b/frontend/src/app/main/ui/ds/buttons/button.scss
new file mode 100644
index 0000000000..5e7b2cfe63
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/buttons/button.scss
@@ -0,0 +1,35 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+//
+// Copyright (c) KALEIDOS INC
+
+@use "../typography.scss" as *;
+@use "./buttons" as *;
+
+.button {
+ @extend %base-button;
+
+ @include use-typography("headline-small");
+ padding: 0 var(--sp-m);
+
+ display: inline-flex;
+ align-items: center;
+ column-gap: var(--sp-xs);
+}
+
+.button-primary {
+ @extend %base-button-primary;
+}
+
+.button-secondary {
+ @extend %base-button-secondary;
+}
+
+.button-ghost {
+ @extend %base-button-ghost;
+}
+
+.button-destructive {
+ @extend %base-button-destructive;
+}
diff --git a/frontend/src/app/main/ui/ds/buttons/button.stories.jsx b/frontend/src/app/main/ui/ds/buttons/button.stories.jsx
new file mode 100644
index 0000000000..8a2c78a159
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/buttons/button.stories.jsx
@@ -0,0 +1,74 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+//
+// Copyright (c) KALEIDOS INC
+
+import * as React from "react";
+import Components from "@target/components";
+
+const { Button } = Components;
+const { icons } = Components.meta;
+
+const iconList = [
+ ...Object.entries(icons)
+ .map(([_, value]) => value)
+ .sort(),
+];
+
+export default {
+ title: "Buttons/Button",
+ component: Components.Button,
+ argTypes: {
+ icon: {
+ options: iconList,
+ control: { type: "select" },
+ },
+ disabled: { control: "boolean" },
+ variant: {
+ options: ["primary", "secondary", "ghost", "destructive"],
+ control: { type: "select" },
+ },
+ },
+ args: {
+ children: "Lorem ipsum",
+ disabled: false,
+ variant: undefined,
+ },
+ parameters: {
+ controls: { exclude: ["children"] },
+ },
+ render: ({ ...args }) => ,
+};
+
+export const Default = {};
+
+export const WithIcon = {
+ args: {
+ icon: "effects",
+ },
+};
+
+export const Primary = {
+ args: {
+ variant: "primary",
+ },
+};
+
+export const Secondary = {
+ args: {
+ variant: "secondary",
+ },
+};
+
+export const Ghost = {
+ args: {
+ variant: "ghost",
+ },
+};
+
+export const Destructive = {
+ args: {
+ variant: "destructive",
+ },
+};
diff --git a/frontend/src/app/main/ui/ds/buttons/buttons.mdx b/frontend/src/app/main/ui/ds/buttons/buttons.mdx
new file mode 100644
index 0000000000..3bc00dc93b
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/buttons/buttons.mdx
@@ -0,0 +1,69 @@
+import { Canvas, Meta } from '@storybook/blocks';
+import * as ButtonStories from "./button.stories";
+import * as IconButtonStories from "./icon_button.stories";
+
+
+
+# Buttons
+
+Buttons trigger an action such as submitting a form or showing/hiding an interface component.
+
+## Variants
+
+**Primary** (`"primary"`), used to initiate the main / primary action of a view or flow. Avoid to have more than one primary buttons in the same view or flow.
+
+
+
+**Secondary** (`"secondary"`), the default and most common buttons in the interface. Use them for non primary actions.
+
+
+
+**Ghosts** (`"ghost"`), used for less prominent, and sometimes independent, actions (examples: add pages, add properties, etc.)
+
+
+
+**Destructive** (`"destructive"`), used for any action that destroys any object or data. Don't use them for actions like dettach, unlink, etc.
+
+
+
+## Technical notes
+
+### Icons
+
+Both `button*` and `icon-button*` accept an `icon` prop, which must contain an [icon ID](../foundations/assets/icon.mdx).
+These are available in the `app.main.ds.foundations.assets.icon` namespace.
+
+```clj
+(ns app.main.ui.foo
+ (:require
+ [app.main.ui.ds.foundations.assets.icon :as i]))
+```
+
+```clj
+[:> button* {:icon i/effects} "Lorem ipsum"]
+```
+
+
+
+### Accessibility
+
+Icon buttons require an `aria-label`. This is also shown in a tooltip on hovering the button.
+
+```clj
+[:> icon-button* {:icon i/effects :aria-label "Lorem ipsum"}]
+```
+
+
+
+## Usage guidelines (design)
+
+### Where to use
+
+Penpot app has a high-density interface, so use buttons thoughtfully to establish
+a clear and logical visual hierarchy, and avoid overwhelming the user.
+
+### When to use
+
+Buttons can be used in forms, navigation links or anywhere that needs simple,
+standard button functionality. Used also to grab users' attention (i.e. navigate
+to main user flows like register, etc.)
diff --git a/frontend/src/app/main/ui/ds/buttons/icon_button.cljs b/frontend/src/app/main/ui/ds/buttons/icon_button.cljs
new file mode 100644
index 0000000000..addfc63725
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/buttons/icon_button.cljs
@@ -0,0 +1,30 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) KALEIDOS INC
+
+(ns app.main.ui.ds.buttons.icon-button
+ (:require-macros
+ [app.common.data.macros :as dm]
+ [app.main.style :as stl])
+ (:require
+ [app.main.ui.ds.foundations.assets.icon :refer [icon*]]
+ [rumext.v2 :as mf]))
+
+(def button-variants (set '("primary" "secondary" "ghost" "destructive")))
+
+(mf/defc icon-button*
+ {::mf/props :obj}
+ [{:keys [class icon variant aria-label] :rest props}]
+ (assert (or (not variant) (contains? button-variants variant)) "invalid variant")
+ (assert (some? aria-label) "aria-label must be provided")
+ (assert (some? icon) "an icon id must be provided")
+ (let [variant (or variant "primary")
+ class (dm/str class " " (stl/css-case :icon-button true
+ :icon-button-primary (= variant "primary")
+ :icon-button-secondary (= variant "secondary")
+ :icon-button-ghost (= variant "ghost")
+ :icon-button-destructive (= variant "destructive")))
+ props (mf/spread-props props {:class class :title aria-label})]
+ [:> "button" props [:> icon* {:id icon :aria-label aria-label}]]))
\ No newline at end of file
diff --git a/frontend/src/app/main/ui/ds/buttons/icon_button.scss b/frontend/src/app/main/ui/ds/buttons/icon_button.scss
new file mode 100644
index 0000000000..1a10c3775b
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/buttons/icon_button.scss
@@ -0,0 +1,33 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+//
+// Copyright (c) KALEIDOS INC
+
+@use "../typography.scss" as *;
+@use "../_sizes.scss" as *;
+@use "./buttons" as *;
+
+.icon-button {
+ @extend %base-button;
+ width: #{$sz-32};
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.icon-button-primary {
+ @extend %base-button-primary;
+}
+
+.icon-button-secondary {
+ @extend %base-button-secondary;
+}
+
+.icon-button-ghost {
+ @extend %base-button-ghost;
+}
+
+.icon-button-destructive {
+ @extend %base-button-destructive;
+}
diff --git a/frontend/src/app/main/ui/ds/buttons/icon_button.stories.jsx b/frontend/src/app/main/ui/ds/buttons/icon_button.stories.jsx
new file mode 100644
index 0000000000..17cb4b2fbb
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/buttons/icon_button.stories.jsx
@@ -0,0 +1,66 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+//
+// Copyright (c) KALEIDOS INC
+
+import * as React from "react";
+import Components from "@target/components";
+
+const { IconButton } = Components;
+const { icons } = Components.meta;
+
+const iconList = [
+ ...Object.entries(icons)
+ .map(([_, value]) => value)
+ .sort(),
+];
+
+export default {
+ title: "Buttons/IconButton",
+ component: Components.IconButton,
+ argTypes: {
+ icon: {
+ options: iconList,
+ control: { type: "select" },
+ },
+ disabled: { control: "boolean" },
+ variant: {
+ options: ["primary", "secondary", "ghost", "destructive"],
+ control: { type: "select" },
+ },
+ },
+ args: {
+ disabled: false,
+ variant: undefined,
+ "aria-label": "Lorem ipsum",
+ icon: "effects",
+ },
+ render: ({ ...args }) => ,
+};
+
+export const Default = {};
+
+export const Primary = {
+ args: {
+ variant: "primary",
+ },
+};
+
+export const Secondary = {
+ args: {
+ variant: "secondary",
+ },
+};
+
+export const Ghost = {
+ args: {
+ variant: "ghost",
+ },
+};
+
+export const Destructive = {
+ args: {
+ variant: "destructive",
+ },
+};