diff --git a/inc/Plugin.php b/inc/Plugin.php
index 48196f3..ae6befd 100644
--- a/inc/Plugin.php
+++ b/inc/Plugin.php
@@ -69,6 +69,7 @@ public function register_blocks(): void {
$blocks = [
'carousel',
'carousel/controls',
+ 'carousel/counter',
'carousel/dots',
'carousel/viewport',
'carousel/slide',
diff --git a/src/blocks/carousel/counter/block.json b/src/blocks/carousel/counter/block.json
new file mode 100644
index 0000000..8b8bc49
--- /dev/null
+++ b/src/blocks/carousel/counter/block.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "version": "1.0.0",
+ "name": "carousel-kit/carousel-counter",
+ "title": "Carousel Counter",
+ "category": "carousel-kit",
+ "icon": "editor-ol",
+ "ancestor": [
+ "carousel-kit/carousel"
+ ],
+ "description": "Displays current slide number and total slides (e.g., 1 of 3).",
+ "textdomain": "carousel-kit",
+ "attributes": {},
+ "supports": {
+ "interactivity": true
+ },
+ "editorScript": "file:./index.js",
+ "style": "file:./style-index.css"
+}
diff --git a/src/blocks/carousel/counter/edit.tsx b/src/blocks/carousel/counter/edit.tsx
new file mode 100644
index 0000000..f42d5ab
--- /dev/null
+++ b/src/blocks/carousel/counter/edit.tsx
@@ -0,0 +1,52 @@
+import { useBlockProps } from '@wordpress/block-editor';
+import { useEffect, useState, useContext } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import { EditorCarouselContext } from '../editor-context';
+
+export default function Edit() {
+ const blockProps = useBlockProps( {
+ className: 'carousel-kit-counter',
+ } );
+
+ const { emblaApi } = useContext( EditorCarouselContext );
+ const [ totalSlides, setTotalSlides ] = useState( 0 );
+ const [ selectedIndex, setSelectedIndex ] = useState( 0 );
+
+ useEffect( () => {
+ if ( ! emblaApi ) {
+ return;
+ }
+
+ const onInit = () => {
+ setTotalSlides( emblaApi.slideNodes().length );
+ setSelectedIndex( emblaApi.selectedScrollSnap() );
+ };
+
+ const onSelect = () => {
+ setSelectedIndex( emblaApi.selectedScrollSnap() );
+ };
+
+ emblaApi.on( 'init', onInit );
+ emblaApi.on( 'reInit', onInit );
+ emblaApi.on( 'select', onSelect );
+
+ onInit(); // Initial load.
+
+ return () => {
+ emblaApi.off( 'init', onInit );
+ emblaApi.off( 'reInit', onInit );
+ emblaApi.off( 'select', onSelect );
+ };
+ }, [ emblaApi ] );
+
+ // Calculate current slide (1-based) - fallback to 1 if no slides yet.
+ const currentSlide = selectedIndex + 1;
+
+ return (
+
+ { currentSlide }
+ { __( 'of', 'carousel-kit' ) }
+ { totalSlides || 1 }
+
+ );
+}
diff --git a/src/blocks/carousel/counter/index.ts b/src/blocks/carousel/counter/index.ts
new file mode 100644
index 0000000..90d7f18
--- /dev/null
+++ b/src/blocks/carousel/counter/index.ts
@@ -0,0 +1,11 @@
+import { registerBlockType, type BlockConfiguration } from '@wordpress/blocks';
+import Edit from './edit';
+import Save from './save';
+import metadata from './block.json';
+import type { CarouselCounterAttributes } from '../types';
+import './style.scss';
+
+registerBlockType( metadata as BlockConfiguration, {
+ edit: Edit,
+ save: Save,
+} );
diff --git a/src/blocks/carousel/counter/save.tsx b/src/blocks/carousel/counter/save.tsx
new file mode 100644
index 0000000..1d67d05
--- /dev/null
+++ b/src/blocks/carousel/counter/save.tsx
@@ -0,0 +1,22 @@
+import { useBlockProps } from '@wordpress/block-editor';
+import { __ } from '@wordpress/i18n';
+
+export default function Save() {
+ const blockProps = useBlockProps.save( {
+ className: 'carousel-kit-counter',
+ } );
+
+ return (
+
+
+ { __( 'of', 'carousel-kit' ) }
+
+
+ );
+}
diff --git a/src/blocks/carousel/counter/style.scss b/src/blocks/carousel/counter/style.scss
new file mode 100644
index 0000000..2b7e61b
--- /dev/null
+++ b/src/blocks/carousel/counter/style.scss
@@ -0,0 +1,19 @@
+/**
+ * Styles for carousel counter/slide indicator
+ */
+.carousel-kit-counter {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--carousel-kit-counter-gap, 0.25rem);
+ font-size: var(--carousel-kit-counter-font-size, inherit);
+ line-height: 1.5;
+ color: var(--carousel-kit-counter-color, inherit);
+}
+
+.carousel-kit-counter__separator {
+ color: var(--carousel-kit-counter-separator-color, inherit);
+}
+
+.carousel-kit-counter__total {
+ color: var(--carousel-kit-counter-total-color, inherit);
+}
diff --git a/src/blocks/carousel/types.ts b/src/blocks/carousel/types.ts
index fe34731..9cc91fe 100644
--- a/src/blocks/carousel/types.ts
+++ b/src/blocks/carousel/types.ts
@@ -23,6 +23,7 @@ export type CarouselViewportAttributes = Record;
export type CarouselSlideAttributes = Record;
export type CarouselControlsAttributes = Record;
export type CarouselDotsAttributes = Record;
+export type CarouselCounterAttributes = Record;
export type CarouselContext = {
options: EmblaOptionsType & {
diff --git a/src/blocks/carousel/view.ts b/src/blocks/carousel/view.ts
index 293aa22..2f7b52b 100644
--- a/src/blocks/carousel/view.ts
+++ b/src/blocks/carousel/view.ts
@@ -133,6 +133,14 @@ store( 'carousel-kit/carousel', {
const index = ( snap?.index || 0 ) + 1;
return context.ariaLabelPattern.replace( '%d', index.toString() );
},
+ getCurrentSlideNumber: () => {
+ const context = getContext();
+ return ( context.selectedIndex + 1 ).toString();
+ },
+ getTotalSlides: () => {
+ const context = getContext();
+ return ( context.scrollSnaps?.length || 1 ).toString();
+ },
initCarousel: () => {
try {
const context = getContext();