Test Organisation October 28, 2026 10 min read

Playwright Tags: How to Tag Tests and Run Them by Tag

Tag Playwright tests as @smoke, @regression or @critical, then run exactly that group with --grep. Current syntax, OR and AND filters, tag-based projects in playwright.config.js, a smoke-on-PR / full-nightly CI setup, and when to use annotations instead. Every example is JavaScript and runs on Playwright 1.62.

Playwright tags let you label tests as @smoke, @regression or @critical and then run just that group from the command line. A 400-test suite that takes 20 minutes becomes a 2-minute smoke check on every pull request, with the full run saved for nightly builds.

This guide shows the current tag syntax, how to run tests by tag with --grep, how to combine tags with OR and AND, how to set up tag-based projects in playwright.config.js, and how tags differ from annotations. Every example is JavaScript, targets a public practice site, and was run on Playwright 1.62.

Quick answer: add { tag: '@smoke' } as the second argument to test(), then run npx playwright test --grep @smoke. Use --grep-invert @slow to exclude a tag.


How to Add Playwright Tags to a Test

Since Playwright 1.42, a test takes an optional details object between the title and the test function. Put your tag (or an array of tags) in it. Every tag must start with @; otherwise Playwright stops with Error: Tag must start with '@'.

tests/tags.spec.js
const { test, expect } = require('@playwright/test');

// One tag
test('home page loads', { tag: '@smoke' }, async ({ page }) => {
  await page.goto('https://playwright.dev/');
  await expect(page).toHaveTitle(/Playwright/);
});

// Multiple tags: pass an array
test('valid user can log in', { tag: ['@smoke', '@critical'] }, async ({ page }) => {
  await page.goto('https://the-internet.herokuapp.com/login');
  await page.getByLabel('Username').fill('tomsmith');
  await page.getByLabel('Password').fill('SuperSecretPassword!');
  await page.getByRole('button', { name: 'Login' }).click();
  await expect(page.locator('#flash')).toContainText('You logged into a secure area!');
});

test('wrong password shows an error', { tag: '@regression' }, async ({ page }) => {
  await page.goto('https://the-internet.herokuapp.com/login');
  await page.getByLabel('Username').fill('tomsmith');
  await page.getByLabel('Password').fill('wrong');
  await page.getByRole('button', { name: 'Login' }).click();
  await expect(page.locator('#flash')).toContainText('Your password is invalid!');
});

In the list reporter, tags are printed after the title, so you can see them in every run:

Output
✓ tags.spec.js:3:1 › home page loads @smoke (1.2s)
✓ tags.spec.js:8:1 › valid user can log in @smoke @critical (4.5s)
✓ tags.spec.js:16:1 › wrong password shows an error @regression (4.3s)

Tag a whole group with test.describe

test.describe accepts the same details object. Every test inside inherits the group tag and can add its own.

tests/todo.spec.js
const { test, expect } = require('@playwright/test');

test.describe('todo list', { tag: '@regression' }, () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://demo.playwright.dev/todomvc');
  });

  // Tags: @regression (inherited) + @smoke
  test('add a todo', { tag: '@smoke' }, async ({ page }) => {
    await page.getByPlaceholder('What needs to be done?').fill('Buy milk');
    await page.getByPlaceholder('What needs to be done?').press('Enter');
    await expect(page.getByTestId('todo-title')).toHaveText(['Buy milk']);
  });

  // Tags: @regression (inherited)
  test('complete a todo', async ({ page }) => {
    await page.getByPlaceholder('What needs to be done?').fill('Walk dog');
    await page.getByPlaceholder('What needs to be done?').press('Enter');
    await page.getByRole('checkbox', { name: 'Toggle Todo' }).check();
    await expect(page.getByTestId('todo-item')).toHaveClass(/completed/);
  });
});

The older style: tags in the title

Before 1.42, teams wrote tags straight into the test title. It still works, because --grep matches against the title text, and you will see it in many existing projects.

tests/legacy-tags.spec.js
const { test, expect } = require('@playwright/test');

// Older style: the tag is part of the title
test('checkboxes can be toggled @regression @slow', async ({ page }) => {
  await page.goto('https://the-internet.herokuapp.com/checkboxes');
  const first = page.getByRole('checkbox').first();
  await first.check();
  await expect(first).toBeChecked();
});

Prefer the { tag } option for new tests: titles stay readable, and you can tag a whole describe block at once. Playwright still recognises @words in titles as tags (they appear in testInfo.tags and --grep finds them), so you can migrate gradually.


Playwright grep: How to Run Tests by Tag

--grep (short form -g) takes a regular expression. Playwright tests it against one string made of the project name, file name, describe titles, test title and tags. That is why tags in the options object and tags in the title are both matched.

Terminal
# Only tests tagged @smoke
npx playwright test --grep @smoke

# Everything except @slow tests
npx playwright test --grep-invert @slow

# Short forms
npx playwright test -g @smoke
npx playwright test -G @slow

# Check what would run, without running it
npx playwright test --grep @smoke --list

Use --list first. It prints the matching tests and the total count without opening a browser. It's the fastest way to check a regex before you put it in CI.

OR: run tests with either tag

Use the regex pipe. Quote it, or your shell treats | as a pipe.

Terminal
# @smoke OR @critical
npx playwright test --grep "@smoke|@critical"

# Exclude two tags at once
npx playwright test --grep-invert "@slow|@flaky"

AND: run tests that have both tags

Regex has no AND operator, so use two lookaheads. Each one checks that its tag appears somewhere in the string, in any order.

Terminal
# @smoke AND @critical
npx playwright test --grep "(?=.*@smoke)(?=.*@critical)"

# @regression but NOT @slow
npx playwright test --grep @regression --grep-invert @slow

On the example files above, the AND pattern returns only valid user can log in, the one test with both tags.

Avoid partial matches

--grep @smoke also matches @smoke-extended, because it's a substring match. If you have tags that share a prefix, anchor the end of the tag:

Terminal
npx playwright test --grep "@smoke(\s|$)"

Note that \b does not help here: there is a word boundary between e and -, so @smoke\b still matches @smoke-extended.

Save the commands you use every day as npm scripts so nobody has to remember the regex:

package.json
"scripts": {
  "test": "playwright test",
  "test:smoke": "playwright test --grep @smoke",
  "test:critical": "playwright test --grep \"(?=.*@smoke)(?=.*@critical)\"",
  "test:fast": "playwright test --grep-invert @slow"
}

For every other way to run a subset (by file, line number, title or project), see how to run Playwright tests.

Know your CLI flags and tags? What does -G do? Does a describe tag reach a test inside it? What happens when you use --grep together with a project that has its own grep? These are the details exam-style questions catch. Test yourself with the Playwright practice tests → Every answer comes with an explanation covering the CLI, config, tags and fixtures.


Tag-Based Projects in playwright.config.js

Instead of typing --grep each time, put the filter in a project. Each project in playwright.config.js can have its own grep and grepInvert (both take a RegExp or an array of RegExps).

playwright.config.js
const { defineConfig, devices } = require('@playwright/test');

module.exports = defineConfig({
  testDir: './tests',
  reporter: 'list',
  projects: [
    {
      name: 'smoke',
      grep: /@smoke/,
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'regression',
      grepInvert: /@slow/,
      use: { ...devices['Desktop Chrome'] },
    },
  ],
});
Terminal
npx playwright test --project=smoke        # 3 tests
npx playwright test --project=regression   # everything except @slow

If you pass --grep on the command line as well, a test has to match both the CLI filter and the project filter. You can also set top-level grep / grepInvert in the config to filter every project.

One more option worth knowing: a top-level tag in the config adds a tag to every test in the run, for example tag: process.env.CI_ENVIRONMENT_NAME. It's useful when you merge reports from several environments and want to tell the runs apart.


CI Strategy: Smoke on PRs, Full Suite Nightly

The most common reason to tag Playwright tests is speed in CI. A practical split:

TriggerWhat runsGoal
Every pull request--grep @smokeFeedback in minutes
Merge to main--grep-invert @slowCatch regressions before release
Nightly scheduleFull suite, all browsersComplete coverage, including slow tests
.github/workflows/playwright.yml
name: Playwright Tests
on:
  pull_request:
  schedule:
    - cron: '0 2 * * *'   # every night at 02:00 UTC

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
      - run: npm ci
      - run: npx playwright install --with-deps

      - name: Smoke tests (pull requests)
        if: github.event_name == 'pull_request'
        run: npx playwright test --grep @smoke

      - name: Full suite (nightly)
        if: github.event_name == 'schedule'
        run: npx playwright test

      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: playwright-report/

When the nightly suite gets long, split it across machines with sharding. The Playwright sharding guide and Playwright GitHub Actions CI/CD guide show the full setup, and the parallel testing guide covers workers.


Playwright Tags vs Annotations

Tags and annotations look similar but do different jobs. Tags are for filtering: which tests run. Annotations are for information or behaviour: a link to a bug, a reason a test is skipped, a note in the report.

tests/annotations.spec.js
const { test, expect } = require('@playwright/test');

test('users API returns 10 users', {
  tag: '@api',
  annotation: { type: 'issue', description: 'https://github.com/your-org/your-repo/issues/42' },
}, async ({ request }, testInfo) => {
  console.log('tags:', testInfo.tags);   // tags: [ '@api' ]
  const response = await request.get('https://jsonplaceholder.typicode.com/users');
  expect(response.ok()).toBeTruthy();
  expect(await response.json()).toHaveLength(10);
});

test('known bug in the cart', { tag: '@regression' }, async ({ page }) => {
  test.fixme(true, 'Cart total is wrong, see issue #57');
  await page.goto('https://demo.playwright.dev/todomvc');
});

Playwright also has built-in annotations that change how a test runs:

AnnotationWhat it does
test.skip()Doesn't run the test. Use for tests that don't apply, e.g. one browser.
test.fixme()Doesn't run the test, and marks it as something to fix.
test.fail()Runs the test and expects it to fail. Passes if it fails.
test.slow()Triples the test timeout.
{ annotation: { type, description } }Custom note shown in the HTML report, such as an issue link.

A simple rule: if you want to run or exclude a group from the command line, use a tag. If you want to explain something about one test, use an annotation.


Filter by Tag in the HTML Report and testInfo.tags

Tags show up as coloured labels in the HTML report. Click a label, or type it in the search box (for example @smoke), to filter the list. That makes it easy to answer "did all the critical tests pass?" after a full run.

Terminal
npx playwright test --reporter=html
npx playwright show-report

Inside a test, hook or fixture, testInfo.tags gives you the test's tags as an array, including tags inherited from describe and @words in the title. You can use it to log tags, send them to a dashboard, or change setup for some groups, for example creating extra test data only for @critical tests. When something tagged fails, the trace viewer and debugging guide show you what happened.


Playwright Tagging Tips

  • Keep the list short. Five to eight tags (@smoke, @regression, @critical, @api, @slow) cover most teams. Fifty tags means nobody knows which to use.
  • Write them down. Put the agreed tags and what each one means in your README.
  • Tag by purpose, not by person or date. @checkout is useful a year from now; @sprint-12 isn't.
  • Keep smoke small. If @smoke takes longer than a few minutes, it has turned into regression.
  • Don't hide flaky tests behind a tag forever. A @flaky tag excluded in CI is fine for a week, not a quarter.

Build a tagged, CI-ready Playwright framework

Hands-on Udemy course in JavaScript: config, projects, Page Objects, API tests, GitHub Actions and AI test generation with Claude and the Playwright MCP Server.

Enroll on Udemy →

Want to check you can answer tag, grep and config questions without looking them up? Try the Playwright Automation Practice Tests: exam-style questions with an explanation for every answer.


Frequently Asked Questions

How do I add a tag to a Playwright test?

Pass a details object as the second argument: test('login works', { tag: '@smoke' }, async ({ page }) => { ... }). For several tags use an array, for example { tag: ['@smoke', '@critical'] }. Tags must start with @ or Playwright throws an error.

How do I run Playwright tests by tag?

Use npx playwright test --grep @smoke (or -g @smoke). To exclude a tag, use --grep-invert @slow (or -G @slow). Add --list to see which tests match without running them.

How do I run tests that have two tags at the same time?

Use regex lookaheads: npx playwright test --grep "(?=.*@smoke)(?=.*@critical)". For either tag, use the pipe: --grep "@smoke|@critical". Quote the pattern so the shell doesn't interpret | or parentheses.

Can I tag a whole describe block in Playwright?

Yes. test.describe('checkout', { tag: '@regression' }, () => { ... }) applies @regression to every test inside. Tests in the block can add their own tags as well.

What is the difference between tags and annotations in Playwright?

Tags are for filtering which tests run, using --grep or grep in the config. Annotations add information or change behaviour: a custom { type, description } annotation shows up in the HTML report, and test.skip, test.fixme, test.fail and test.slow change how the test runs.

Do tags in the test title still work?

Yes. Older projects write tags in the title, such as test('checkout works @smoke'). --grep matches the title text, and Playwright also lists those @words in testInfo.tags. For new tests the { tag } option is cleaner.


Asim Noaman - Playwright and Claude AI course instructor

Asim Noaman

Senior QA Automation Engineer & AI Testing Specialist

With years of hands-on experience building test automation frameworks for production applications, Asim specializes in combining traditional QA methodologies with cutting-edge AI tools. He has helped teams adopt Playwright and AI-driven testing workflows to ship faster with fewer bugs.

Udemy Instructor Published course author
Playwright + AI Expert Specialized in AI-powered QA
Production Experience Enterprise-grade frameworks
Connect on LinkedIn

Playwright + Claude AI Course

Tags Are One Line. A CI-Ready Framework Takes a Plan.

Tagging tests is easy. Knowing what belongs in smoke, how to structure projects, and how to wire it all into a pipeline is the part that takes experience. The course builds a complete Playwright framework in JavaScript with you, including config, Page Objects, API tests, GitHub Actions and AI test generation with Claude and the Playwright MCP Server.

  • Projects, grep filters and a real playwright.config.js
  • GitHub Actions pipeline with reports and traces
  • Claude AI + Playwright MCP Server for writing and fixing tests
  • 4.5★ on Udemy, 510+ students, 30-day money-back guarantee
Enroll Now →

Think you know tags, grep and config? Prove it with the Playwright practice tests: exam-style questions with explanations.