Skip to content

Instantly share code, notes, and snippets.

@htlin222
Created May 14, 2025 14:48
Show Gist options
  • Save htlin222/9861e4747313657598535f76ef4cb09b to your computer and use it in GitHub Desktop.
Save htlin222/9861e4747313657598535f76ef4cb09b to your computer and use it in GitHub Desktop.
Google Slides Custom Progress Bar Generator
function onOpen() {
const ui = SlidesApp.getUi();
ui.createMenu('自定功能')
.addItem('⏳ Reload ProgressBars', 'updateProgressBars')
.addToUi();
}
function updateProgressBars() {
const presentation = SlidesApp.getActivePresentation();
const slides = presentation.getSlides();
const totalSlides = slides.length;
const maxWidth = 720;
const yPosition = 402;
const height = 3;
for (let i = 1; i < totalSlides; i++) { // Start from 2nd slide
const slide = slides[i];
// 1. Remove old progress bar if exists
const shapes = slide.getShapes();
for (let shape of shapes) {
if (shape.getTitle && shape.getTitle() === 'PROGRESS') {
shape.remove();
}
}
// 2. Calculate new width
const progressRatio = i / ( totalSlides - 1);
const barWidth = maxWidth * progressRatio;
// 3. Insert new progress bar
const bar = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, 0, yPosition, barWidth, height);
bar.getFill().setSolidFill('#000000');
bar.getBorder().setTransparent();
// 4. Tag the shape for future identification
bar.setTitle('PROGRESS');
}
}
@htlin222
Copy link
Author

This Google Apps Script adds a custom menu item titled “⏳ Reload ProgressBars” to Google Slides. When triggered, it updates visual progress bars across all slides (excluding the first slide), indicating slide progression. Each bar adjusts its width based on the current slide’s position in the presentation. Existing progress bars are removed and replaced to maintain accuracy.

@raywentsai
Copy link

Brilliant work! This is a fantastic addition to the already excellent "NEJM模版2020新版" — thank you so much for sharing it.
I’ve put together a modified version that omits skipped slides in the progress bar and adds a slide number feature as well.
P.S. The Apps Script is almost entirely "vibe-coded," so the style might not be the cleanest 😄

function onOpen() {
  const ui = SlidesApp.getUi();
  ui.createMenu('自定功能')
    .addItem('⏳ Reload ProgressBars', 'updateProgressBars')
    .addItem('🔢 Update SlideNumbers', 'addSlideNumbersSkippingSkipped')
    .addItem('✅ Update Both', 'updateAll')
    .addToUi();
}

function updateProgressBars() {
  const presentation = SlidesApp.getActivePresentation();
  const slides = presentation.getSlides();

  // Build list of non-skipped slide indexes (excluding 1st slide)
  const nonSkippedIndexes = slides
    .map((slide, index) => ({ slide, index }))
    .filter(({ slide, index }) => index > 0 && !slide.isSkipped())
    .map(({ index }) => index);

  const totalNonSkipped = nonSkippedIndexes.length;

  const maxWidth = 720;
  const yPosition = 402;
  const height = 3;

  if (totalNonSkipped <= 1) return; // nothing to show progress for

  for (let i = 1; i < slides.length; i++) { // Start from 2nd slide
    const slide = slides[i];

    // Remove old progress bar if exists
    const shapes = slide.getShapes();
    for (let shape of shapes) {
      if (shape.getTitle && shape.getTitle() === 'PROGRESS') {
        shape.remove();
      }
    }

    // Skip if slide is marked as skipped
    if (slide.isSkipped()) continue;

    // Find progress position in the non-skipped slide sequence
    const progressPosition = nonSkippedIndexes.indexOf(i) + 1; // progressPosition starts from 0
    const progressRatio = progressPosition / (totalNonSkipped); // totalNonSkipped excluded 1st slide
    const barWidth = maxWidth * progressRatio;

    const bar = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, 0, yPosition, barWidth, height);
    bar.getFill().setSolidFill('#000000');
    bar.getBorder().setTransparent();
    
    bar.setTitle('PROGRESS');
  }
}

function addSlideNumbersSkippingSkipped() {
  const presentation = SlidesApp.getActivePresentation();
  const slides = presentation.getSlides();

  // Get indexes of non-skipped slides (excluding title slide)
  const nonSkippedIndexes = slides
    .map((slide, index) => ({ slide, index }))
    .filter(({ slide, index }) => index > 0 && !slide.isSkipped())
    .map(({ index }) => index);

  const fontSize = 18;
  const fontFamily = "Cambria";
  const x = 670;
  const y = 365;

  // Clear existing slide numbers
  slides.forEach(slide => {
    slide.getShapes().forEach(shape => {
      if (shape.getTitle?.() === 'SLIDE_NUMBER') {
        shape.remove();
      }
    });
  });

  // Insert new slide numbers based on non-skipped order
  nonSkippedIndexes.forEach((slideIndex, i) => {
    const slide = slides[slideIndex];
    const shape = slide.insertTextBox(`${i + 1}`, x, y, 40, 30);
    shape.getText().getTextStyle().setFontSize(fontSize);
    shape.getText().getTextStyle().setFontFamily(fontFamily);
    shape.getText().getParagraphStyle().setParagraphAlignment(SlidesApp.ParagraphAlignment.END);
    shape.setTitle('SLIDE_NUMBER');
  });
}

function updateAll() {
  updateProgressBars();
  addSlideNumbersSkippingSkipped();
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment