diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5c89823376..b18c817fb6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
- Modernized `dcc.Tabs`
- Modernized `dcc.DatePickerSingle` and `dcc.DatePickerRange`
- DatePicker calendars can now accept translations as an external script, either with Dash's `external_scripts` or from the assets folder. See [documentation](https://date-fns.org/v4.1.0/docs/CDN) for the underlying library that supports this.
+- New `dcc.Button` component that mirrors `html.Button` but with default styles applied
## Changed
- `dcc.Tab` now accepts a `width` prop which can be a pixel or percentage width for an individual tab.
diff --git a/components/dash-core-components/src/components/Button.tsx b/components/dash-core-components/src/components/Button.tsx
new file mode 100644
index 0000000000..e129302628
--- /dev/null
+++ b/components/dash-core-components/src/components/Button.tsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import {ButtonProps} from '../types';
+import './css/button.css';
+
+/**
+ * Similar to dash.html.Button, but with theming and styles applied.
+ */
+const Button = ({
+ setProps,
+ n_blur = 0,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ n_blur_timestamp = -1,
+ n_clicks = 0,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ n_clicks_timestamp = -1,
+ type = 'button',
+ className,
+ children,
+ ...props
+}: ButtonProps) => {
+ const ctx = window.dash_component_api.useDashContext();
+ const isLoading = ctx.useLoading();
+
+ return (
+
+ );
+};
+
+export default Button;
diff --git a/components/dash-core-components/src/components/css/button.css b/components/dash-core-components/src/components/css/button.css
new file mode 100644
index 0000000000..92594df6e3
--- /dev/null
+++ b/components/dash-core-components/src/components/css/button.css
@@ -0,0 +1,55 @@
+.dash-button {
+ line-height: 32px;
+ background: color-mix(
+ in srgb,
+ var(--Dash-Fill-Interactive-Strong) 5%,
+ transparent
+ );
+ color: var(--Dash-Fill-Interactive-Strong);
+ padding: 0 calc(var(--Dash-Spacing) * 2);
+ border-radius: 4px;
+ border: 1px solid var(--Dash-Fill-Interactive-Strong);
+ box-sizing: border-box;
+ vertical-align: middle;
+}
+
+/* Hover state - stronger background */
+.dash-button:hover {
+ cursor: pointer;
+ background: var(--Dash-Fill-Inverse-Strong);
+ color: var(--Dash-Fill-Interactive-Strong);
+}
+
+/* Active state (clicking) - inverted colors */
+.dash-button:active {
+ background: var(--Dash-Fill-Interactive-Strong);
+ color: var(--Dash-Fill-Inverse-Strong);
+}
+
+/* Keyboard focus - inverted colors */
+.dash-button:focus-visible {
+ outline: none;
+ background: var(--Dash-Fill-Interactive-Strong);
+ color: var(--Dash-Fill-Inverse-Strong);
+}
+
+/* Hover after keyboard focus - keep inverted but acknowledge hover */
+.dash-button:focus-visible:hover {
+ background: var(--Dash-Fill-Interactive-Strong);
+ color: var(--Dash-Fill-Inverse-Strong);
+}
+
+/* Active state after keyboard focus - inverted colors */
+.dash-button:focus-visible:active {
+ background: var(--Dash-Fill-Interactive-Strong);
+ color: var(--Dash-Fill-Inverse-Strong);
+}
+
+/* Disabled state */
+.dash-button:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ background: var(--Dash-Fill-Disabled);
+ color: var(--Dash-Text-Strong);
+ border-color: var(--Dash-Stroke-Weak);
+}
diff --git a/components/dash-core-components/src/index.ts b/components/dash-core-components/src/index.ts
index 80c5c6f0ec..a2555149d4 100644
--- a/components/dash-core-components/src/index.ts
+++ b/components/dash-core-components/src/index.ts
@@ -1,4 +1,5 @@
/* eslint-disable import/prefer-default-export */
+import Button from './components/Button';
import Checklist from './components/Checklist';
import Clipboard from './components/Clipboard.react';
import ConfirmDialog from './components/ConfirmDialog.react';
@@ -28,6 +29,7 @@ import Upload from './components/Upload.react';
import './components/css/dcc.css';
export {
+ Button,
Checklist,
Clipboard,
ConfirmDialog,
diff --git a/components/dash-core-components/src/types.ts b/components/dash-core-components/src/types.ts
index b337c59b2f..72d8aab961 100644
--- a/components/dash-core-components/src/types.ts
+++ b/components/dash-core-components/src/types.ts
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, {ButtonHTMLAttributes, DetailedHTMLProps} from 'react';
import {BaseDashProps, DashComponent} from '@dash-renderer/types';
export enum PersistenceTypes {
@@ -52,6 +52,117 @@ export interface BaseDccProps
persistence_type?: PersistenceTypes;
}
+export type ButtonProps = BaseDccProps & {
+ /**
+ * The children of this component.
+ */
+ children?: React.ReactNode;
+ /**
+ * Defines the type of the element.
+ */
+ type?: 'submit' | 'reset' | 'button';
+ /**
+ * The element should be automatically focused after the page loaded.
+ */
+ autoFocus?: boolean;
+ /**
+ * Indicates whether the user can interact with the element.
+ */
+ disabled?: boolean;
+ /**
+ * Indicates the form that is the owner of the element.
+ */
+ form?: string;
+ /**
+ * Indicates the action of the element, overriding the action defined in the