diff --git a/package-lock.json b/package-lock.json
index 12e24e5195f3d1..a9142e287a6aac 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10533,6 +10533,7 @@
"@babel/runtime": "^7.8.3",
"@wordpress/block-editor": "file:packages/block-editor",
"@wordpress/components": "file:packages/components",
+ "@wordpress/data": "file:packages/data",
"@wordpress/dom": "file:packages/dom",
"@wordpress/element": "file:packages/element",
"@wordpress/html-entities": "file:packages/html-entities",
diff --git a/packages/block-editor/src/components/rich-text/format-toolbar/index.js b/packages/block-editor/src/components/rich-text/format-toolbar/index.js
index 1667bc2a06b042..ee9cd480fc40a3 100644
--- a/packages/block-editor/src/components/rich-text/format-toolbar/index.js
+++ b/packages/block-editor/src/components/rich-text/format-toolbar/index.js
@@ -19,12 +19,14 @@ const FormatToolbar = () => {
return (
- { [ 'bold', 'italic', 'link' ].map( ( format ) => (
-
- ) ) }
+ { [ 'bold', 'italic', 'link', 'text-color' ].map(
+ ( format ) => (
+
+ )
+ ) }
{ ( fills ) =>
fills.length !== 0 && (
diff --git a/packages/format-library/package.json b/packages/format-library/package.json
index a970f595c36ea2..e106f3abdd17f1 100644
--- a/packages/format-library/package.json
+++ b/packages/format-library/package.json
@@ -24,6 +24,7 @@
"@babel/runtime": "^7.8.3",
"@wordpress/block-editor": "file:../block-editor",
"@wordpress/components": "file:../components",
+ "@wordpress/data": "file:../data",
"@wordpress/dom": "file:../dom",
"@wordpress/element": "file:../element",
"@wordpress/html-entities": "file:../html-entities",
diff --git a/packages/format-library/src/default-formats.js b/packages/format-library/src/default-formats.js
index 889998635d3942..ce6a9b5f73679b 100644
--- a/packages/format-library/src/default-formats.js
+++ b/packages/format-library/src/default-formats.js
@@ -8,5 +8,15 @@ import { italic } from './italic';
import { link } from './link';
import { strikethrough } from './strikethrough';
import { underline } from './underline';
+import { textColor } from './text-color';
-export default [ bold, code, image, italic, link, strikethrough, underline ];
+export default [
+ bold,
+ code,
+ image,
+ italic,
+ link,
+ strikethrough,
+ underline,
+ textColor,
+];
diff --git a/packages/format-library/src/style.scss b/packages/format-library/src/style.scss
index a9ab600a7ad7d1..9aae705658e945 100644
--- a/packages/format-library/src/style.scss
+++ b/packages/format-library/src/style.scss
@@ -1,2 +1,3 @@
@import "./image/style.scss";
@import "./link/style.scss";
+@import "./text-color/style.scss";
diff --git a/packages/format-library/src/text-color/index.js b/packages/format-library/src/text-color/index.js
new file mode 100644
index 00000000000000..467467553f05cd
--- /dev/null
+++ b/packages/format-library/src/text-color/index.js
@@ -0,0 +1,94 @@
+/**
+ * External dependencies
+ */
+import { get } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useSelect } from '@wordpress/data';
+import { useCallback, useMemo, useState } from '@wordpress/element';
+import { RichTextToolbarButton } from '@wordpress/block-editor';
+import { Dashicon } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import { default as InlineColorUI, getActiveColor } from './inline';
+
+const name = 'core/text-color';
+const title = __( 'Text Color' );
+
+const EMPTY_ARRAY = [];
+
+function TextColorEdit( { value, onChange, isActive, activeAttributes } ) {
+ const colors = useSelect( ( select ) => {
+ const { getSettings } = select( 'core/block-editor' );
+ if ( getSettings ) {
+ return get( getSettings(), [ 'colors' ], EMPTY_ARRAY );
+ }
+ return EMPTY_ARRAY;
+ } );
+ const [ isAddingColor, setIsAddingColor ] = useState( false );
+ const enableIsAddingColor = useCallback( () => setIsAddingColor( true ), [
+ setIsAddingColor,
+ ] );
+ const disableIsAddingColor = useCallback( () => setIsAddingColor( false ), [
+ setIsAddingColor,
+ ] );
+ const colorIndicatorStyle = useMemo( () => {
+ const activeColor = getActiveColor( name, value, colors );
+ if ( ! activeColor ) {
+ return undefined;
+ }
+ return {
+ backgroundColor: activeColor,
+ };
+ }, [ value, colors ] );
+ return (
+ <>
+
+
+ { isActive && (
+
+ ) }
+ >
+ }
+ title={ title }
+ onClick={ enableIsAddingColor }
+ />
+ { isAddingColor && (
+
+ ) }
+ >
+ );
+}
+
+export const textColor = {
+ name,
+ title,
+ tagName: 'span',
+ className: 'has-inline-color',
+ attributes: {
+ style: 'style',
+ class: 'class',
+ },
+ edit: TextColorEdit,
+};
diff --git a/packages/format-library/src/text-color/inline.js b/packages/format-library/src/text-color/inline.js
new file mode 100644
index 00000000000000..db40c8367687fb
--- /dev/null
+++ b/packages/format-library/src/text-color/inline.js
@@ -0,0 +1,137 @@
+/**
+ * External dependencies
+ */
+import { get } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { useCallback, useMemo } from '@wordpress/element';
+import { useSelect } from '@wordpress/data';
+import { withSpokenMessages } from '@wordpress/components';
+import { getRectangleFromRange } from '@wordpress/dom';
+import {
+ applyFormat,
+ removeFormat,
+ getActiveFormat,
+} from '@wordpress/rich-text';
+import {
+ ColorPalette,
+ URLPopover,
+ getColorClassName,
+ getColorObjectByColorValue,
+ getColorObjectByAttributeValues,
+} from '@wordpress/block-editor';
+
+export function getActiveColor( formatName, formatValue, colors ) {
+ const activeColorFormat = getActiveFormat( formatValue, formatName );
+ if ( ! activeColorFormat ) {
+ return;
+ }
+ const styleColor = activeColorFormat.attributes.style;
+ if ( styleColor ) {
+ return styleColor.replace( new RegExp( `^color:\\s*` ), '' );
+ }
+ const currentClass = activeColorFormat.attributes.class;
+ if ( currentClass ) {
+ const colorSlug = currentClass.replace( /.*has-(.*?)-color.*/, '$1' );
+ return getColorObjectByAttributeValues( colors, colorSlug ).color;
+ }
+}
+
+const ColorPopoverAtLink = ( { isActive, addingColor, value, ...props } ) => {
+ const anchorRect = useMemo( () => {
+ const selection = window.getSelection();
+ const range =
+ selection.rangeCount > 0 ? selection.getRangeAt( 0 ) : null;
+ if ( ! range ) {
+ return;
+ }
+
+ if ( addingColor ) {
+ return getRectangleFromRange( range );
+ }
+
+ let element = range.startContainer;
+
+ // If the caret is right before the element, select the next element.
+ element = element.nextElementSibling || element;
+
+ while ( element.nodeType !== window.Node.ELEMENT_NODE ) {
+ element = element.parentNode;
+ }
+
+ const closest = element.closest( 'span' );
+ if ( closest ) {
+ return closest.getBoundingClientRect();
+ }
+ }, [ isActive, addingColor, value.start, value.end ] );
+
+ if ( ! anchorRect ) {
+ return null;
+ }
+
+ return ;
+};
+
+const ColorPicker = ( { name, value, onChange } ) => {
+ const colors = useSelect( ( select ) => {
+ const { getSettings } = select( 'core/block-editor' );
+ return get( getSettings(), [ 'colors' ], [] );
+ } );
+ const onColorChange = useCallback(
+ ( color ) => {
+ if ( color ) {
+ const colorObject = getColorObjectByColorValue( colors, color );
+ onChange(
+ applyFormat( value, {
+ type: name,
+ attributes: colorObject
+ ? {
+ class: getColorClassName(
+ 'color',
+ colorObject.slug
+ ),
+ }
+ : {
+ style: `color:${ color }`,
+ },
+ } )
+ );
+ } else {
+ onChange( removeFormat( value, name ) );
+ }
+ },
+ [ colors, onChange ]
+ );
+ const activeColor = useMemo( () => getActiveColor( name, value, colors ), [
+ name,
+ value,
+ colors,
+ ] );
+
+ return ;
+};
+
+const InlineColorUI = ( {
+ name,
+ value,
+ onChange,
+ onClose,
+ isActive,
+ addingColor,
+} ) => {
+ return (
+
+
+
+ );
+};
+
+export default withSpokenMessages( InlineColorUI );
diff --git a/packages/format-library/src/text-color/style.scss b/packages/format-library/src/text-color/style.scss
new file mode 100644
index 00000000000000..0a988ae9b313f1
--- /dev/null
+++ b/packages/format-library/src/text-color/style.scss
@@ -0,0 +1,43 @@
+.components-inline-color__indicator {
+ position: absolute;
+ background: #000;
+ height: 3px;
+ width: 20px;
+ bottom: 6px;
+ left: auto;
+ right: auto;
+ margin: 0 5px;
+}
+
+.components-inline-color-popover {
+
+ .components-popover__content {
+ padding: 20px 18px;
+
+ .components-color-palette {
+ margin-top: 0.6rem;
+ }
+
+ .components-base-control__title {
+ display: block;
+ margin-bottom: 16px;
+ font-weight: 600;
+ color: #191e23;
+ }
+
+ .component-color-indicator {
+ vertical-align: text-bottom;
+ }
+ }
+}
+
+.format-library-text-color-button {
+ position: relative;
+}
+.format-library-text-color-button__indicator {
+ height: 4px;
+ width: 20px;
+ position: absolute;
+ bottom: 6px;
+ left: 8px;
+}