diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md
index 10ceb797e28c0f..39c2fd1ac556e2 100644
--- a/docs/reference-guides/core-blocks.md
+++ b/docs/reference-guides/core-blocks.md
@@ -700,6 +700,15 @@ Add text that respects your spacing and tabs, and also allows styling. ([Source]
- **Supports:** anchor, color (background, gradients, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight)
- **Attributes:** content
+## Progress Bar
+
+Display a progress bar. Useful for tracking progress on a task or project. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/progress-bar))
+
+- **Name:** core/progress-bar
+- **Category:** design
+- **Supports:** ~~html~~
+- **Attributes:** backgroundColor, height, isReadProgress, label, max, progressColor, seprator, showTotal, showValue, symbol, symbolPosition, value
+
## Pullquote
Give special visual emphasis to a quote from your text. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/pullquote))
diff --git a/lib/blocks.php b/lib/blocks.php
index 342cd25191e689..3efeda6bd08435 100644
--- a/lib/blocks.php
+++ b/lib/blocks.php
@@ -43,6 +43,7 @@ function gutenberg_reregister_core_block_types() {
'verse',
'video',
'embed',
+ 'progress-bar',
),
'block_names' => array(
'archives.php' => 'core/archives',
diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js
index 262f11de6ee22d..0c25f6c0593c74 100644
--- a/packages/block-library/src/index.js
+++ b/packages/block-library/src/index.js
@@ -4,8 +4,8 @@
import {
setDefaultBlockName,
setFreeformContentHandlerName,
- setUnregisteredTypeHandlerName,
setGroupingBlockName,
+ setUnregisteredTypeHandlerName,
} from '@wordpress/blocks';
/**
@@ -21,17 +21,16 @@ import {
//
// See https://github.com/WordPress/gutenberg/pull/40655 for more context.
import * as archives from './archives';
-import * as avatar from './avatar';
import * as audio from './audio';
+import * as avatar from './avatar';
+import * as reusableBlock from './block';
import * as button from './button';
import * as buttons from './buttons';
import * as calendar from './calendar';
import * as categories from './categories';
-import * as classic from './freeform';
import * as code from './code';
import * as column from './column';
import * as columns from './columns';
-import * as comments from './comments';
import * as commentAuthorAvatar from './comment-author-avatar';
import * as commentAuthorName from './comment-author-name';
import * as commentContent from './comment-content';
@@ -39,19 +38,22 @@ import * as commentDate from './comment-date';
import * as commentEditLink from './comment-edit-link';
import * as commentReplyLink from './comment-reply-link';
import * as commentTemplate from './comment-template';
-import * as commentsPaginationPrevious from './comments-pagination-previous';
+import * as comments from './comments';
import * as commentsPagination from './comments-pagination';
import * as commentsPaginationNext from './comments-pagination-next';
import * as commentsPaginationNumbers from './comments-pagination-numbers';
+import * as commentsPaginationPrevious from './comments-pagination-previous';
import * as commentsTitle from './comments-title';
import * as cover from './cover';
import * as details from './details';
import * as embed from './embed';
import * as file from './file';
+import * as footnotes from './footnotes';
import * as form from './form';
import * as formInput from './form-input';
-import * as formSubmitButton from './form-submit-button';
import * as formSubmissionNotification from './form-submission-notification';
+import * as formSubmitButton from './form-submit-button';
+import * as classic from './freeform';
import * as gallery from './gallery';
import * as group from './group';
import * as heading from './heading';
@@ -70,13 +72,13 @@ import * as navigation from './navigation';
import * as navigationLink from './navigation-link';
import * as navigationSubmenu from './navigation-submenu';
import * as nextpage from './nextpage';
-import * as pattern from './pattern';
import * as pageList from './page-list';
import * as pageListItem from './page-list-item';
import * as paragraph from './paragraph';
+import * as pattern from './pattern';
import * as postAuthor from './post-author';
-import * as postAuthorName from './post-author-name';
import * as postAuthorBiography from './post-author-biography';
+import * as postAuthorName from './post-author-name';
import * as postComment from './post-comment';
import * as postCommentsCount from './post-comments-count';
import * as postCommentsForm from './post-comments-form';
@@ -91,6 +93,7 @@ import * as postTerms from './post-terms';
import * as postTimeToRead from './post-time-to-read';
import * as postTitle from './post-title';
import * as preformatted from './preformatted';
+import * as progressBar from './progress-bar';
import * as pullquote from './pullquote';
import * as query from './query';
import * as queryNoResults from './query-no-results';
@@ -101,7 +104,6 @@ import * as queryPaginationPrevious from './query-pagination-previous';
import * as queryTitle from './query-title';
import * as queryTotal from './query-total';
import * as quote from './quote';
-import * as reusableBlock from './block';
import * as readMore from './read-more';
import * as rss from './rss';
import * as search from './search';
@@ -121,7 +123,6 @@ import * as termDescription from './term-description';
import * as textColumns from './text-columns';
import * as verse from './verse';
import * as video from './video';
-import * as footnotes from './footnotes';
import isBlockMetadataExperimental from './utils/is-block-metadata-experimental';
@@ -182,6 +183,7 @@ const getAllBlocks = () => {
verse,
video,
footnotes,
+ progressBar,
// theme blocks
navigation,
diff --git a/packages/block-library/src/progress-bar/block.json b/packages/block-library/src/progress-bar/block.json
new file mode 100644
index 00000000000000..bb5f96a644f2c7
--- /dev/null
+++ b/packages/block-library/src/progress-bar/block.json
@@ -0,0 +1,63 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "core/progress-bar",
+ "category": "design",
+ "title": "Progress Bar",
+ "description": "Display a progress bar. Useful for tracking progress on a task or project.",
+ "textdomain": "default",
+ "attributes": {
+ "label": {
+ "type": "string",
+ "default": ""
+ },
+ "value": {
+ "type": "number",
+ "default": 50
+ },
+ "max": {
+ "type": "number",
+ "default": 100
+ },
+ "backgroundColor": {
+ "type": "string",
+ "default": "#f0f0f0"
+ },
+ "progressColor": {
+ "type": "string",
+ "default": "#1E1E1E"
+ },
+ "height": {
+ "type": "number",
+ "default": 11
+ },
+ "showValue": {
+ "type": "boolean",
+ "default": true
+ },
+ "isReadProgress": {
+ "type": "boolean",
+ "default": false
+ },
+ "symbol": {
+ "type": "string",
+ "default": "%"
+ },
+ "symbolPosition": {
+ "type": "string",
+ "default": "suffix"
+ },
+ "showTotal": {
+ "type": "boolean",
+ "default": false
+ },
+ "seprator": {
+ "type": "string",
+ "default": "/"
+ }
+ },
+ "supports": {
+ "html": false
+ },
+ "style": "wp-block-progress-bar"
+}
diff --git a/packages/block-library/src/progress-bar/edit.js b/packages/block-library/src/progress-bar/edit.js
new file mode 100644
index 00000000000000..36dde1da405674
--- /dev/null
+++ b/packages/block-library/src/progress-bar/edit.js
@@ -0,0 +1,368 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ InspectorControls,
+ PanelColorSettings,
+ RichText,
+ useBlockProps,
+} from '@wordpress/block-editor';
+import {
+ __experimentalNumberControl as NumberControl,
+ RangeControl,
+ SelectControl,
+ TextControl,
+ ToggleControl,
+ __experimentalToolsPanel as ToolsPanel,
+ __experimentalToolsPanelItem as ToolsPanelItem,
+} from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import { useToolsPanelDropdownMenuProps } from '../utils/hooks';
+
+export default function Edit( { attributes, setAttributes } ) {
+ const {
+ label,
+ value,
+ max = 100,
+ backgroundColor,
+ progressColor,
+ height,
+ showValue,
+ isReadProgress,
+ symbol,
+ symbolPosition,
+ showTotal,
+ seprator,
+ } = attributes;
+
+ const blockProps = useBlockProps( {
+ className: 'wp-block-progress-bar',
+ } );
+
+ const progressBarStyle = {
+ backgroundColor,
+ height: `${ height }px`,
+ };
+
+ const progressStyle = {
+ backgroundColor: progressColor,
+ width: `${ ( value / max ) * 100 }%`,
+ };
+
+ const readProgressStyle = {
+ backgroundColor: progressColor,
+ height: `${ height }px`,
+ };
+
+ const dropdownMenuProps = useToolsPanelDropdownMenuProps();
+
+ const formatValue = ( val ) => {
+ return symbolPosition === 'prefix'
+ ? `${ symbol }${ val }`
+ : `${ val }${ symbol }`;
+ };
+
+ const valueDisplay = showTotal
+ ? `${ formatValue( value ) } ${ seprator } ${ formatValue( max ) }`
+ : formatValue( value );
+
+ return (
+
+
+ {
+ setAttributes( {
+ label: '',
+ value: 50,
+ max: 100,
+ backgroundColor: '#f0f0f0',
+ progressColor: '#1E1E1E',
+ height: 11,
+ showValue: true,
+ isReadProgress: false,
+ symbol: '%',
+ symbolPosition: 'suffix',
+ showTotal: false,
+ seprator: '/',
+ } );
+ } }
+ dropdownMenuProps={ dropdownMenuProps }
+ >
+ isReadProgress }
+ onDeselect={ () =>
+ setAttributes( { isReadProgress: false } )
+ }
+ >
+
+ setAttributes( {
+ isReadProgress: ! isReadProgress,
+ } )
+ }
+ />
+
+ { ! isReadProgress && (
+ <>
+ value !== 50 }
+ onDeselect={ () =>
+ setAttributes( { value: 50 } )
+ }
+ >
+
+ setAttributes( { value: currentValue } )
+ }
+ min={ 0 }
+ max={ max }
+ />
+
+ max !== 100 }
+ onDeselect={ () =>
+ setAttributes( { max: 100 } )
+ }
+ >
+
+ setAttributes( { max: maxValue } )
+ }
+ />
+
+ >
+ ) }
+ height !== 11 }
+ onDeselect={ () => setAttributes( { height: 11 } ) }
+ >
+
+ setAttributes( { height: heightValue } )
+ }
+ min={ 1 }
+ max={ 30 }
+ />
+
+
+ { ! isReadProgress && (
+ <>
+ ! showValue }
+ onDeselect={ () =>
+ setAttributes( { showValue: true } )
+ }
+ >
+
+ setAttributes( {
+ showValue: ! showValue,
+ } )
+ }
+ />
+
+ { showValue && (
+ showTotal }
+ onDeselect={ () =>
+ setAttributes( { showTotal: false } )
+ }
+ >
+
+ setAttributes( {
+ showTotal: ! showTotal,
+ } )
+ }
+ />
+
+ ) }
+ { showValue && showTotal && (
+ seprator !== '/' }
+ onDeselect={ () =>
+ setAttributes( { seprator: '/' } )
+ }
+ >
+
+ setAttributes( {
+ seprator: newSeprator,
+ } )
+ }
+ />
+
+ ) }
+ { showValue && (
+ <>
+ symbol !== '%' }
+ onDeselect={ () =>
+ setAttributes( { symbol: '%' } )
+ }
+ >
+
+ setAttributes( {
+ symbol: newSymbol,
+ } )
+ }
+ />
+
+
+ symbolPosition !== 'suffix'
+ }
+ onDeselect={ () =>
+ setAttributes( {
+ symbolPosition: 'suffix',
+ } )
+ }
+ >
+
+ setAttributes( {
+ symbolPosition: position,
+ } )
+ }
+ />
+
+ >
+ ) }
+ >
+ ) }
+
+
+ setAttributes( { backgroundColor: bgColor } ),
+ label: __( 'Background Color' ),
+ },
+ {
+ value: progressColor,
+ onChange: ( progressBarColor ) =>
+ setAttributes( {
+ progressColor: progressBarColor,
+ } ),
+ label: __( 'Progress Color' ),
+ },
+ ] }
+ />
+
+
+
+ { ! isReadProgress && (
+
+
+ setAttributes( { label: content } )
+ }
+ placeholder={ __( 'Write heading…' ) }
+ />
+ { showValue && { valueDisplay }
}
+
+ ) }
+
+
+
+ );
+}
diff --git a/packages/block-library/src/progress-bar/index.js b/packages/block-library/src/progress-bar/index.js
new file mode 100644
index 00000000000000..b7f1257c98b142
--- /dev/null
+++ b/packages/block-library/src/progress-bar/index.js
@@ -0,0 +1,30 @@
+/**
+ * WordPress dependencies
+ */
+import { _x } from '@wordpress/i18n';
+import { progressBar as icon } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import initBlock from '../utils/init-block';
+import metadata from './block.json';
+import edit from './edit';
+import save from './save';
+
+const { name } = metadata;
+
+export { metadata, name };
+
+export const settings = {
+ icon,
+ edit,
+ save,
+ example: {
+ attributes: {
+ label: _x( 'Progress Bar', 'block example' ),
+ },
+ },
+};
+
+export const init = () => initBlock( { name, metadata, settings } );
diff --git a/packages/block-library/src/progress-bar/init.js b/packages/block-library/src/progress-bar/init.js
new file mode 100644
index 00000000000000..a7f22ef02d640f
--- /dev/null
+++ b/packages/block-library/src/progress-bar/init.js
@@ -0,0 +1,6 @@
+/**
+ * Internal dependencies
+ */
+import { init } from '.';
+
+export default init();
diff --git a/packages/block-library/src/progress-bar/save.js b/packages/block-library/src/progress-bar/save.js
new file mode 100644
index 00000000000000..375ae2921f43e1
--- /dev/null
+++ b/packages/block-library/src/progress-bar/save.js
@@ -0,0 +1,87 @@
+/**
+ * WordPress dependencies
+ */
+import { RichText, useBlockProps } from '@wordpress/block-editor';
+
+export default function Save( { attributes } ) {
+ const {
+ label,
+ value,
+ max = 100,
+ backgroundColor,
+ progressColor,
+ height,
+ showValue,
+ isReadProgress,
+ symbol,
+ symbolPosition,
+ showTotal,
+ seprator,
+ } = attributes;
+
+ // eslint-disable-next-line react-compiler/react-compiler
+ const blockProps = useBlockProps.save( {
+ className: 'wp-block-progress-bar',
+ } );
+
+ const progressBarStyle = {
+ backgroundColor,
+ height: `${ height }px`,
+ };
+
+ const progressStyle = {
+ backgroundColor: progressColor,
+ width: `${ ( value / max ) * 100 }%`,
+ };
+
+ const readProgressStyle = {
+ backgroundColor: progressColor,
+ height: `${ height }px`,
+ };
+
+ const formatValue = ( val ) => {
+ return symbolPosition === 'prefix'
+ ? `${ symbol }${ val }`
+ : `${ val }${ symbol }`;
+ };
+
+ const valueDisplay = showTotal
+ ? `${ formatValue( value ) } ${ seprator } ${ formatValue( max ) }`
+ : formatValue( value );
+
+ return (
+
+
+ { ! isReadProgress && (
+
+
+ { showValue &&
{ valueDisplay }
}
+
+ ) }
+
+
+
+ );
+}
diff --git a/packages/block-library/src/progress-bar/style.scss b/packages/block-library/src/progress-bar/style.scss
new file mode 100644
index 00000000000000..9427e4c028677d
--- /dev/null
+++ b/packages/block-library/src/progress-bar/style.scss
@@ -0,0 +1,77 @@
+@keyframes grow-progress {
+ from {
+ transform: scaleX(0);
+ }
+ to {
+ transform: scaleX(1);
+ }
+}
+
+.wp-block-progress-bar {
+ &__container {
+ width: 100%;
+ }
+
+ &__container > div:first-child {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ &__bar {
+ width: 100%;
+ overflow: hidden;
+ position: relative;
+ }
+
+ &__progress {
+ height: 100%;
+
+ @media not (prefers-reduced-motion) {
+ transition: width 0.3s ease;
+ }
+ }
+
+ &__read-bar {
+ width: 100%;
+ position: fixed;
+ left: 0;
+ top: 0;
+ z-index: 1000;
+
+ .admin-bar & {
+ top: 32px;
+
+ @media screen and (max-width: 779px) {
+ top: 46px;
+ }
+
+ @media screen and (max-width: 594px) {
+ top: 0;
+ }
+ }
+ }
+
+ &__read-progress {
+ position: fixed;
+ left: 0;
+ top: 0;
+ width: 100%;
+ transform-origin: 0 50%;
+ animation: grow-progress auto linear;
+ z-index: 1000;
+ animation-timeline: scroll() !important;
+
+ .admin-bar & {
+ top: 32px;
+
+ @media screen and (max-width: 779px) {
+ top: 46px;
+ }
+
+ @media screen and (max-width: 594px) {
+ top: 0;
+ }
+ }
+ }
+}
diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss
index c61049c23151b9..298371ff72dee2 100644
--- a/packages/block-library/src/style.scss
+++ b/packages/block-library/src/style.scss
@@ -70,5 +70,6 @@
@import "./verse/style.scss";
@import "./video/style.scss";
@import "./footnotes/style.scss";
+@import "./progress-bar/style.scss";
@import "common.scss";
diff --git a/packages/icons/src/index.js b/packages/icons/src/index.js
index e82b09e5d5afe9..fad74665820624 100644
--- a/packages/icons/src/index.js
+++ b/packages/icons/src/index.js
@@ -218,6 +218,7 @@ export { default as previous } from './library/previous';
export { default as next } from './library/next';
export { default as offline } from './library/offline';
export { default as preformatted } from './library/preformatted';
+export { default as progressBar } from './library/progress-bar';
export { default as published } from './library/published';
export { default as pullLeft } from './library/pull-left';
export { default as pullRight } from './library/pull-right';
diff --git a/packages/icons/src/library/progress-bar.js b/packages/icons/src/library/progress-bar.js
new file mode 100644
index 00000000000000..ae16345ba45f2c
--- /dev/null
+++ b/packages/icons/src/library/progress-bar.js
@@ -0,0 +1,23 @@
+/**
+ * WordPress dependencies
+ */
+import { G, Path, SVG } from '@wordpress/primitives';
+
+const progressBar = (
+
+);
+
+export default progressBar;
diff --git a/test/e2e/specs/editor/blocks/progress.spec.js b/test/e2e/specs/editor/blocks/progress.spec.js
new file mode 100644
index 00000000000000..0acda6fc003722
--- /dev/null
+++ b/test/e2e/specs/editor/blocks/progress.spec.js
@@ -0,0 +1,66 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+test.describe( 'Progress Bar', () => {
+ test.beforeEach( async ( { admin } ) => {
+ await admin.createNewPost();
+ } );
+
+ test.afterEach( async ( { requestUtils } ) => {
+ await requestUtils.deleteAllPosts();
+ } );
+
+ test( 'should render progress bar with correct prefix symbol and value', async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.insertBlock( { name: 'core/progress-bar' } );
+ await page.getByLabel( 'Value Symbol' ).click();
+ await page.getByLabel( 'Value Symbol' ).fill( '$' );
+ await page.getByLabel( 'Symbol Position' ).selectOption( 'prefix' );
+
+ const editorFrame = page
+ .locator( 'iframe[name="editor-canvas"]' )
+ .contentFrame();
+
+ const progressBarValue = editorFrame
+ .locator( '.wp-block-progress-bar__container > div > p' )
+ .nth( 1 );
+
+ await expect( progressBarValue ).toHaveText( '$50' );
+ } );
+
+ test( 'should apply custom background and progress colors', async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.insertBlock( { name: 'core/progress-bar' } );
+
+ await page.getByRole( 'button', { name: 'Background Color' } ).click();
+ await page.getByLabel( 'Cyan bluish gray' ).click();
+ await page.getByRole( 'button', { name: 'Progress Color' } ).click();
+ await page.getByLabel( 'Vivid purple' ).click();
+
+ const editorFrame = page
+ .locator( 'iframe[name="editor-canvas"]' )
+ .contentFrame();
+
+ const barContainer = editorFrame.locator(
+ '.wp-block-progress-bar__bar'
+ );
+ const progressBar = editorFrame.locator(
+ '.wp-block-progress-bar__progress'
+ );
+
+ await expect( barContainer ).toHaveCSS(
+ 'background-color',
+ 'rgb(171, 184, 195)'
+ );
+ await expect( progressBar ).toHaveCSS(
+ 'background-color',
+ 'rgb(155, 81, 224)'
+ );
+ } );
+} );