diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md
index 43aabae2aca086..73872bc67d3aab 100644
--- a/docs/reference-guides/core-blocks.md
+++ b/docs/reference-guides/core-blocks.md
@@ -45,6 +45,16 @@ Create and save content to reuse across your site. Update the block, and the cha
- **Supports:** ~~customClassName~~, ~~html~~, ~~inserter~~
- **Attributes:** ref
+## Breadcrumbs
+
+Displays breadcrumbs of a page's hierarchy, or a post's categories ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/breadcrumbs))
+
+- **Name:** core/breadcrumbs
+- **Experimental:** fse
+- **Category:** theme
+- **Supports:** align (full, wide), color (link, text, ~~background~~), typography (fontSize, lineHeight), ~~html~~
+- **Attributes:** contentJustification, separator, showCurrentPageTitle, showLeadingSeparator, showSiteTitle, siteTitleOverride
+
## Button
Prompt visitors to take action with a button-style link. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/button))
diff --git a/lib/blocks.php b/lib/blocks.php
index fdbc555a2f9443..f355095cc2eb04 100644
--- a/lib/blocks.php
+++ b/lib/blocks.php
@@ -49,6 +49,7 @@ function gutenberg_reregister_core_block_types() {
'archives.php' => 'core/archives',
'avatar.php' => 'core/avatar',
'block.php' => 'core/block',
+ 'breadcrumbs.php' => 'core/breadcrumbs',
'calendar.php' => 'core/calendar',
'categories.php' => 'core/categories',
'cover.php' => 'core/cover',
diff --git a/packages/block-library/src/breadcrumbs/block.json b/packages/block-library/src/breadcrumbs/block.json
new file mode 100644
index 00000000000000..7beed377ea5758
--- /dev/null
+++ b/packages/block-library/src/breadcrumbs/block.json
@@ -0,0 +1,60 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "__experimental": "fse",
+ "name": "core/breadcrumbs",
+ "title": "Breadcrumbs",
+ "category": "theme",
+ "description": "Displays breadcrumbs of a page's hierarchy, or a post's categories",
+ "textdomain": "default",
+ "usesContext": [ "postId", "postType" ],
+ "attributes": {
+ "contentJustification": {
+ "type": "string"
+ },
+ "separator": {
+ "type": "string",
+ "default": "/"
+ },
+ "showCurrentPageTitle": {
+ "type": "boolean",
+ "default": false
+ },
+ "showLeadingSeparator": {
+ "type": "boolean",
+ "default": false
+ },
+ "showSiteTitle": {
+ "type": "boolean",
+ "default": true
+ },
+ "siteTitleOverride": {
+ "type": "string"
+ }
+ },
+ "supports": {
+ "align": [ "wide", "full" ],
+ "color": {
+ "background": false,
+ "link": true,
+ "__experimentalDefaultControls": {
+ "text": true,
+ "link": true
+ }
+ },
+ "html": false,
+ "typography": {
+ "fontSize": true,
+ "lineHeight": true,
+ "__experimentalFontFamily": true,
+ "__experimentalFontStyle": true,
+ "__experimentalFontWeight": true,
+ "__experimentalTextTransform": true,
+ "__experimentalDefaultControls": {
+ "fontSize": true
+ }
+ }
+ },
+ "editorStyle": "wp-block-breadcrumbs-editor",
+ "style": "wp-block-breadcrumbs"
+}
diff --git a/packages/block-library/src/breadcrumbs/edit.js b/packages/block-library/src/breadcrumbs/edit.js
new file mode 100644
index 00000000000000..7cf7048c4db188
--- /dev/null
+++ b/packages/block-library/src/breadcrumbs/edit.js
@@ -0,0 +1,311 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+
+/**
+ * WordPress dependencies
+ */
+import { useSelect } from '@wordpress/data';
+import {
+ BlockControls,
+ InspectorControls,
+ JustifyContentControl,
+ RichText,
+ useBlockProps,
+} from '@wordpress/block-editor';
+import { PanelBody, ToggleControl } from '@wordpress/components';
+import { useEffect, useState, useMemo } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import { store as coreStore } from '@wordpress/core-data';
+import { decodeEntities } from '@wordpress/html-entities';
+
+function Breadcrumb( {
+ addLeadingSeparator,
+ crumbTitle,
+ editableTitleField = undefined,
+ isSelected,
+ placeholder,
+ separator,
+ setAttributes,
+ showSeparator,
+} ) {
+ let crumbAnchor;
+ let separatorSpan;
+
+ // Keep track of whether or not the title field has been edited.
+ // This allows the default site title to be rendered as full "real" text.
+ // Then, when it's edited, if the title is removed, it is displayed as a placeholder,
+ // until the block is de-selected, where it is then treated as real text again.
+ const [ isDirty, setIsDirty ] = useState();
+
+ useEffect( () => {
+ if ( ! isSelected ) {
+ setIsDirty( false );
+ }
+ }, [ isSelected ] );
+
+ if ( separator || isSelected ) {
+ separatorSpan = (
+
+
+ setAttributes( { separator: html } )
+ }
+ />
+
+ );
+ }
+
+ if ( editableTitleField ) {
+ /* eslint-disable jsx-a11y/anchor-is-valid */
+ crumbAnchor = (
+ event.preventDefault() }>
+ { isSelected ? (
+ {
+ setIsDirty( true );
+ setAttributes( { [ editableTitleField ]: html } );
+ } }
+ />
+ ) : (
+ crumbTitle || placeholder
+ ) }
+
+ );
+ /* eslint-enable */
+ } else if ( crumbTitle ) {
+ /* eslint-disable jsx-a11y/anchor-is-valid */
+ crumbAnchor = (
+ event.preventDefault() }>
+ { crumbTitle }
+
+ );
+ /* eslint-enable */
+ }
+
+ return (
+
+ { addLeadingSeparator ? separatorSpan : null }
+ { crumbAnchor }
+ { showSeparator ? separatorSpan : null }
+
+ );
+}
+
+export default function BreadcrumbsEdit( {
+ attributes,
+ isSelected,
+ setAttributes,
+ context: { postType, postId },
+} ) {
+ const {
+ contentJustification,
+ separator,
+ showCurrentPageTitle,
+ showLeadingSeparator,
+ showSiteTitle,
+ siteTitleOverride,
+ } = attributes;
+
+ const { categories, parents, post, siteTitle } = useSelect(
+ ( select ) => {
+ const { getEntityRecord, getEditedEntityRecord } =
+ select( coreStore );
+
+ const siteData = getEntityRecord( 'root', '__unstableBase' );
+ const currentPost = getEditedEntityRecord(
+ 'postType',
+ postType,
+ postId
+ );
+
+ const parentCategories = [];
+ const parentEntities = [];
+ let categoryId = currentPost?.categories?.[ 0 ];
+ let currentParentId = currentPost?.parent;
+
+ while ( currentParentId ) {
+ const nextParent = getEntityRecord(
+ 'postType',
+ postType,
+ currentParentId
+ );
+
+ currentParentId = null;
+
+ if ( nextParent ) {
+ parentEntities.push( nextParent );
+ currentParentId = nextParent?.parent || null;
+ }
+ }
+
+ while ( categoryId ) {
+ const nextCategory = getEntityRecord(
+ 'taxonomy',
+ 'category',
+ categoryId
+ );
+
+ categoryId = null;
+
+ if ( nextCategory ) {
+ parentCategories.push( nextCategory );
+ categoryId = nextCategory?.parent || null;
+ }
+ }
+
+ return {
+ categories: parentCategories,
+ post: currentPost,
+ parents: parentEntities.reverse(),
+ siteTitle: decodeEntities( siteData?.name ),
+ };
+ },
+ [ postId, postType ]
+ );
+
+ // Construct breadcrumbs.
+ const breadcrumbs = useMemo( () => {
+ // Set breadcrumb names to real hierarchical post titles if available, and
+ // fall back to category names, or placeholder content if neither exists.
+
+ const crumbs = [];
+ let breadcrumbTitles;
+
+ if ( parents?.length ) {
+ breadcrumbTitles = parents.map(
+ ( parent ) => parent?.title?.rendered || ' '
+ );
+ } else if ( categories?.length ) {
+ breadcrumbTitles = categories.map(
+ ( category ) => category?.name || ' '
+ );
+ } else {
+ breadcrumbTitles = [ __( 'Top-level page' ), __( 'Child page' ) ];
+ }
+
+ // Prepend the site title or site title override if specified.
+ if ( showSiteTitle && siteTitle ) {
+ crumbs.push(
+
+ );
+ }
+
+ // Append current page title if set.
+ if ( showCurrentPageTitle ) {
+ breadcrumbTitles.push( post?.title || __( 'Current page' ) );
+ }
+
+ breadcrumbTitles.forEach( ( item, index ) => {
+ crumbs.push(
+
+ );
+ } );
+
+ return crumbs;
+ }, [
+ categories,
+ isSelected,
+ parents,
+ post?.title,
+ separator,
+ setAttributes,
+ showCurrentPageTitle,
+ showLeadingSeparator,
+ showSiteTitle,
+ siteTitle,
+ siteTitleOverride,
+ ] );
+
+ const blockProps = useBlockProps( {
+ className: classnames( {
+ [ `is-content-justification-${ contentJustification }` ]:
+ contentJustification,
+ } ),
+ } );
+
+ return (
+ <>
+
+
+ setAttributes( { contentJustification: value } )
+ }
+ popoverProps={ {
+ position: 'bottom right',
+ isAlternate: true,
+ } }
+ />
+
+
+
+
+ setAttributes( {
+ showLeadingSeparator: ! showLeadingSeparator,
+ } )
+ }
+ />
+
+ setAttributes( {
+ showCurrentPageTitle: ! showCurrentPageTitle,
+ } )
+ }
+ />
+
+ setAttributes( {
+ showSiteTitle: ! showSiteTitle,
+ } )
+ }
+ />
+
+
+
+ >
+ );
+}
diff --git a/packages/block-library/src/breadcrumbs/editor.scss b/packages/block-library/src/breadcrumbs/editor.scss
new file mode 100644
index 00000000000000..551a1edcb9c92b
--- /dev/null
+++ b/packages/block-library/src/breadcrumbs/editor.scss
@@ -0,0 +1,11 @@
+/**
+ * Editor only CSS.
+ */
+
+// Undo default editor styles.
+// These need extra specificity.
+.editor-styles-wrapper .wp-block-breadcrumbs {
+ ol {
+ padding-left: 0;
+ }
+}
diff --git a/packages/block-library/src/breadcrumbs/index.js b/packages/block-library/src/breadcrumbs/index.js
new file mode 100644
index 00000000000000..d8de83008cfc84
--- /dev/null
+++ b/packages/block-library/src/breadcrumbs/index.js
@@ -0,0 +1,21 @@
+/**
+ * WordPress dependencies
+ */
+import { listView as icon } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import initBlock from '../utils/init-block';
+import metadata from './block.json';
+import edit from './edit';
+
+const { name } = metadata;
+export { metadata, name };
+
+export const settings = {
+ icon,
+ edit,
+};
+
+export const init = () => initBlock( { name, metadata, settings } );
diff --git a/packages/block-library/src/breadcrumbs/index.php b/packages/block-library/src/breadcrumbs/index.php
new file mode 100644
index 00000000000000..9036a1556b13a4
--- /dev/null
+++ b/packages/block-library/src/breadcrumbs/index.php
@@ -0,0 +1,218 @@
+context['postId'] ) ) {
+ return '';
+ }
+
+ $post_id = $block->context['postId'];
+ $post_type = get_post_type( $post_id );
+
+ if ( false === $post_type ) {
+ return '';
+ }
+
+ $ancestor_ids = array();
+ $has_post_hierarchy = is_post_type_hierarchical( $post_type );
+ $show_site_title = ! empty( $attributes['showSiteTitle'] );
+ $show_current_page = ! empty( $attributes['showCurrentPageTitle'] );
+
+ if ( $has_post_hierarchy ) {
+ $ancestor_ids = get_post_ancestors( $post_id );
+
+ if (
+ empty( $ancestor_ids ) &&
+ ! ( $show_site_title && $show_current_page )
+ ) {
+ return '';
+ }
+ } else {
+ $terms = get_the_terms( $post_id, 'category' );
+
+ if ( empty( $terms ) || is_wp_error( $terms ) ) {
+ return '';
+ }
+
+ $term = get_term( $terms[0], 'category' );
+
+ $ancestor_ids[] = $term->term_id;
+ $ancestor_ids = array_merge( $ancestor_ids, get_ancestors( $term->term_id, 'category' ) );
+ }
+
+ $breadcrumbs = array();
+
+ // Prepend site title breadcrumb if available and set to show.
+ $site_title = get_bloginfo( 'name' );
+ if ( $site_title && $show_site_title ) {
+ $site_title = ! empty( $attributes['siteTitleOverride'] ) ?
+ $attributes['siteTitleOverride'] :
+ $site_title;
+
+ $breadcrumbs[] = array(
+ 'url' => get_bloginfo( 'url' ),
+ 'title' => $site_title,
+ );
+ }
+
+ if ( $has_post_hierarchy ) {
+ // Construct remaining breadcrumbs from ancestor ids.
+ foreach ( array_reverse( $ancestor_ids ) as $ancestor_id ) {
+ $breadcrumbs[] = array(
+ 'url' => get_the_permalink( $ancestor_id ),
+ 'title' => get_the_title( $ancestor_id ),
+ );
+ }
+ } else {
+ foreach ( array_reverse( $ancestor_ids ) as $ancestor_id ) {
+ $breadcrumbs[] = array(
+ 'url' => get_category_link( $ancestor_id ),
+ 'title' => get_cat_name( $ancestor_id ),
+ );
+ }
+ }
+
+ // Append current page title if set to show.
+ if ( $show_current_page ) {
+ $breadcrumbs[] = array(
+ 'url' => get_the_permalink( $post_id ),
+ 'title' => get_the_title( $post_id ),
+ );
+ }
+
+ $inner_markup = '';
+
+ /**
+ * Filters the list of breadcrumb links within the Breadcrumbs block render callback.
+ *
+ * @since 6.3.0
+ *
+ * @param array[] An array of Breadcrumb arrays with `url` and `title` keys.
+ */
+ $breadcrumbs = apply_filters( 'block_core_breadcrumbs_links', $breadcrumbs );
+
+ foreach ( $breadcrumbs as $index => $breadcrumb ) {
+ $show_separator = $index < count( $breadcrumbs ) - 1;
+ $inner_markup .= build_block_core_breadcrumbs_inner_markup_item(
+ $breadcrumb['url'],
+ $breadcrumb['title'],
+ $attributes,
+ $index,
+ $show_separator,
+ ( $show_current_page && count( $breadcrumbs ) - 1 === $index )
+ );
+ }
+
+ $classnames = '';
+
+ if ( ! empty( $attributes['contentJustification'] ) ) {
+ if ( 'left' === $attributes['contentJustification'] ) {
+ $classnames = 'is-content-justification-left';
+ }
+
+ if ( 'center' === $attributes['contentJustification'] ) {
+ $classnames = 'is-content-justification-center';
+ }
+
+ if ( 'right' === $attributes['contentJustification'] ) {
+ $classnames = 'is-content-justification-right';
+ }
+ }
+
+ $wrapper_attributes = get_block_wrapper_attributes(
+ array(
+ 'class' => $classnames,
+ 'aria-label' => __( 'Breadcrumb' ),
+ )
+ );
+
+ return sprintf(
+ '',
+ $wrapper_attributes,
+ $inner_markup
+ );
+}
+
+/**
+ * Builds the markup for a single Breadcrumb item.
+ *
+ * Used when iterating over a list of breadcrumb urls and titles.
+ *
+ * @param string $url The url for the link in the breadcrumb.
+ * @param string $title The label/title for the breadcrumb item.
+ * @param array $attributes Block attributes.
+ * @param int $index The position in a list of ids.
+ * @param bool $show_separator Whether to show the separator character where available.
+ * @param bool $is_current_page Whether to mark the breadcrumb item as the current page.
+ *
+ * @return string The markup for a single breadcrumb item wrapped in an `li` element.
+ */
+function build_block_core_breadcrumbs_inner_markup_item( $url, $title, $attributes, $index, $show_separator = true, $is_current_page = false ) {
+ $li_class = 'wp-block-breadcrumbs__item';
+ $separator_class = 'wp-block-breadcrumbs__separator';
+
+ $markup = '';
+
+ // Render leading separator if specified.
+ if (
+ ! empty( $attributes['showLeadingSeparator'] ) &&
+ ! empty( $attributes['separator'] ) &&
+ 0 === $index
+ ) {
+ $markup .= sprintf(
+ '%2$s',
+ $separator_class,
+ wp_kses_post( $attributes['separator'] )
+ );
+ }
+
+ $markup .= sprintf(
+ '%s',
+ esc_url( $url ),
+ $is_current_page ? ' aria-current="page"' : '',
+ wp_kses_post( $title )
+ );
+
+ if (
+ $show_separator &&
+ ! empty( $attributes['separator'] )
+ ) {
+ $markup .= sprintf(
+ '%2$s',
+ $separator_class,
+ wp_kses_post( $attributes['separator'] )
+ );
+ }
+
+ return sprintf(
+ '%2$s',
+ $li_class,
+ $markup
+ );
+}
+
+/**
+ * Registers the `core/post-title` block on the server.
+ */
+function register_block_core_breadcrumbs() {
+ register_block_type_from_metadata(
+ __DIR__ . '/breadcrumbs',
+ array(
+ 'render_callback' => 'render_block_core_breadcrumbs',
+ )
+ );
+}
+add_action( 'init', 'register_block_core_breadcrumbs' );
diff --git a/packages/block-library/src/breadcrumbs/style.scss b/packages/block-library/src/breadcrumbs/style.scss
new file mode 100644
index 00000000000000..bcf742537b2c58
--- /dev/null
+++ b/packages/block-library/src/breadcrumbs/style.scss
@@ -0,0 +1,27 @@
+.wp-block-breadcrumbs {
+ ol {
+ list-style-type: none;
+ padding-left: 0;
+ margin: 0;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5em;
+ }
+
+ &.is-content-justification-left ol {
+ justify-content: flex-start;
+ }
+
+ &.is-content-justification-center ol {
+ justify-content: center;
+ }
+
+ &.is-content-justification-right ol {
+ justify-content: flex-end;
+ }
+
+ .wp-block-breadcrumbs__item {
+ display: flex;
+ gap: 0.5em;
+ }
+}
diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss
index 07c58599c50980..61ab2eb41a87e0 100644
--- a/packages/block-library/src/editor.scss
+++ b/packages/block-library/src/editor.scss
@@ -2,6 +2,7 @@
@import "./audio/editor.scss";
@import "./avatar/editor.scss";
@import "./block/editor.scss";
+@import "./breadcrumbs/editor.scss";
@import "./button/editor.scss";
@import "./buttons/editor.scss";
@import "./categories/editor.scss";
diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js
index 73c2f1eb1140a2..456337aa61263b 100644
--- a/packages/block-library/src/index.js
+++ b/packages/block-library/src/index.js
@@ -116,6 +116,7 @@ import * as termDescription from './term-description';
import * as textColumns from './text-columns';
import * as verse from './verse';
import * as video from './video';
+import * as breadcrumbs from './breadcrumbs';
import isBlockMetadataExperimental from './utils/is-block-metadata-experimental';
@@ -226,6 +227,7 @@ const getAllBlocks = () => {
termDescription,
queryTitle,
postAuthorBiography,
+ breadcrumbs,
];
return blocks.filter( Boolean );
};
diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss
index c5277995d10dab..5318e12cce8863 100644
--- a/packages/block-library/src/style.scss
+++ b/packages/block-library/src/style.scss
@@ -1,6 +1,7 @@
@import "./archives/style.scss";
@import "./avatar/style.scss";
@import "./audio/style.scss";
+@import "./breadcrumbs/style.scss";
@import "./button/style.scss";
@import "./buttons/style.scss";
@import "./calendar/style.scss";
diff --git a/test/integration/fixtures/blocks/core__breadcrumbs.html b/test/integration/fixtures/blocks/core__breadcrumbs.html
new file mode 100644
index 00000000000000..80bbc89da1daa9
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__breadcrumbs.html
@@ -0,0 +1 @@
+
diff --git a/test/integration/fixtures/blocks/core__breadcrumbs.json b/test/integration/fixtures/blocks/core__breadcrumbs.json
new file mode 100644
index 00000000000000..a57d7b17ab55f2
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__breadcrumbs.json
@@ -0,0 +1,13 @@
+[
+ {
+ "name": "core/breadcrumbs",
+ "isValid": true,
+ "attributes": {
+ "separator": "|",
+ "showCurrentPageTitle": false,
+ "showLeadingSeparator": true,
+ "showSiteTitle": true
+ },
+ "innerBlocks": []
+ }
+]
diff --git a/test/integration/fixtures/blocks/core__breadcrumbs.parsed.json b/test/integration/fixtures/blocks/core__breadcrumbs.parsed.json
new file mode 100644
index 00000000000000..1cd27717679445
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__breadcrumbs.parsed.json
@@ -0,0 +1,12 @@
+[
+ {
+ "blockName": "core/breadcrumbs",
+ "attrs": {
+ "separator": "|",
+ "showLeadingSeparator": true
+ },
+ "innerBlocks": [],
+ "innerHTML": "",
+ "innerContent": []
+ }
+]
diff --git a/test/integration/fixtures/blocks/core__breadcrumbs.serialized.html b/test/integration/fixtures/blocks/core__breadcrumbs.serialized.html
new file mode 100644
index 00000000000000..80bbc89da1daa9
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__breadcrumbs.serialized.html
@@ -0,0 +1 @@
+