C# Playwright
last modified October 18, 2026
In this article we show how to automate browsers in C# using the Microsoft Playwright library.
Playwright
Playwright is a cross-browser automation library created by Microsoft. It supports all modern rendering engines including Chromium, WebKit, and Firefox. It is used for end-to-end testing of web applications and for scraping pages that are generated with JavaScript.
Playwright can be used in Node, Python, .NET and Java.
By default, Playwright runs the browser in headless mode, which works without the UI. This is great for scripting and for running tests on servers.
$ dotnet new console -o PlaywrightEx $ cd PlaywrightEx $ dotnet add package Microsoft.Playwright $ dotnet build
We create a console project and add the Playwright package. The build creates
the playwright.ps1 script in the output directory.
$ pwsh bin/Debug/net10.0/playwright.ps1 install chromium
Playwright needs its own builds of the browsers. The script downloads them;
without an argument, it downloads all supported browsers. The script requires
PowerShell; if the pwsh command is not available, PowerShell can
be installed with dotnet tool install --global PowerShell. On
Linux, playwright.ps1 install-deps installs the system libraries
the browsers need.
C# Playwright get title
In the first example, we get the title of a web page.
using Microsoft.Playwright;
using var pw = await Playwright.CreateAsync();
await using var browser = await pw.Chromium.LaunchAsync();
var page = await browser.NewPageAsync();
await page.GotoAsync("https://example.com");
var title = await page.TitleAsync();
Console.WriteLine(title);
The example opens a web page in a browser and retrieves its title.
using var pw = await Playwright.CreateAsync(); await using var browser = await pw.Chromium.LaunchAsync();
We set up Playwright and launch the Chromium browser. Disposing the objects closes the browser.
var page = await browser.NewPageAsync();
The NewPageAsync method creates a new page in a new browser
context. A browser context is like an incognito window: it has its own cookies
and storage.
await page.GotoAsync("https://example.com");
The GotoAsync method navigates to the URL and waits until the page
is loaded.
var title = await page.TitleAsync(); Console.WriteLine(title);
The page title is retrieved with TitleAsync.
$ dotnet run Example Domain
C# Playwright headers
The next example adds headers to the requests of the page.
using Microsoft.Playwright;
using var pw = await Playwright.CreateAsync();
await using var browser = await pw.Chromium.LaunchAsync();
var page = await browser.NewPageAsync();
var headers = new Dictionary<string, string>
{
["User-Agent"] = "C# program",
["X-Request-Source"] = "zetcode"
};
await page.SetExtraHTTPHeadersAsync(headers);
var resp = await page.GotoAsync("https://httpbin.org/headers");
Console.WriteLine($"Status: {resp?.Status}");
var json = await resp!.JsonAsync();
var received = json?.GetProperty("headers");
Console.WriteLine(received?.GetProperty("User-Agent"));
Console.WriteLine(received?.GetProperty("X-Request-Source"));
We set the User-Agent header and a custom header. The
httpbin.org/headers endpoint returns the headers it received as
JSON.
await page.SetExtraHTTPHeadersAsync(headers);
The headers are added to every request that the page makes.
var resp = await page.GotoAsync("https://httpbin.org/headers");
Console.WriteLine($"Status: {resp?.Status}");
var json = await resp!.JsonAsync();
var received = json?.GetProperty("headers");
The GotoAsync method returns the response of the main resource.
The JsonAsync method parses the body into a
JsonElement.
$ dotnet run Status: 200 C# program zetcode
C# Playwright create screenshot
The next example creates screenshots of a web page.
using Microsoft.Playwright;
using var pw = await Playwright.CreateAsync();
await using var browser = await pw.Chromium.LaunchAsync();
var page = await browser.NewPageAsync(new() { ViewportSize = new() { Width = 1280, Height = 800 } });
await page.GotoAsync("https://quotes.toscrape.com");
await page.ScreenshotAsync(new() { Path = "screenshot.png" });
await page.ScreenshotAsync(new() { Path = "fullpage.png", FullPage = true });
Console.WriteLine("screenshots saved");
A screenshot is created with ScreenshotAsync. We set the size of
the viewport when creating the page. The first screenshot contains the visible
part of the page; with the FullPage option, the screenshot
contains the whole scrollable page.
$ dotnet run screenshots saved
To watch the browser at work, launch it with
new() { Headless = false }; this requires a graphical desktop.
C# Playwright locators
Locators are the recommended way to find elements in Playwright. A locator describes how to find an element; the element is looked up each time an action is performed, and Playwright automatically waits until the element is available. Locators can use CSS selectors or find elements by their role, text, or label.
using Microsoft.Playwright;
using var pw = await Playwright.CreateAsync();
await using var browser = await pw.Chromium.LaunchAsync();
var page = await browser.NewPageAsync();
await page.GotoAsync("https://quotes.toscrape.com");
ILocator quotes = page.Locator("div.quote");
Console.WriteLine($"{await quotes.CountAsync()} quotes on the page");
for (int i = 0; i < 3; i++)
{
ILocator quote = quotes.Nth(i);
string author = await quote.Locator("small.author").InnerTextAsync();
IReadOnlyList<string> tags = await quote.Locator("a.tag").AllInnerTextsAsync();
Console.WriteLine($"{author}: {string.Join(", ", tags)}");
}
string first = await quotes.First.Locator("span.text").InnerTextAsync();
Console.WriteLine(first);
We extract quotes from the quotes.toscrape.com website, which is designed for practising web scraping.
ILocator quotes = page.Locator("div.quote");
Console.WriteLine($"{await quotes.CountAsync()} quotes on the page");
The locator matches all div elements with the
quote class. The CountAsync method returns the number
of matching elements.
ILocator quote = quotes.Nth(i);
string author = await quote.Locator("small.author").InnerTextAsync();
IReadOnlyList<string> tags = await quote.Locator("a.tag").AllInnerTextsAsync();
The Nth method selects one of the matches. Locators can be
chained; the inner locators search only inside the quote. The
AllInnerTextsAsync method returns the texts of all matching
elements.
$ dotnet run 10 quotes on the page Albert Einstein: change, deep-thoughts, thinking, world J.K. Rowling: abilities, choices Albert Einstein: inspirational, life, live, miracle, miracles “The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”
C# Playwright click on element
The ClickAsync method clicks an element. In the following
example, we go through the pages of the quotes website by clicking the Next
link.
using Microsoft.Playwright;
using var pw = await Playwright.CreateAsync();
await using var browser = await pw.Chromium.LaunchAsync();
var page = await browser.NewPageAsync();
await page.GotoAsync("https://quotes.toscrape.com");
for (int i = 0; i < 3; i++)
{
await page.GetByRole(AriaRole.Link, new() { Name = "Next" }).ClickAsync();
await page.WaitForURLAsync($"**/page/{i + 2}/");
string author = await page.Locator("small.author").First.InnerTextAsync();
Console.WriteLine($"{page.Url} - first author: {author}");
}
We click the link three times and print the first author of each page.
await page.GetByRole(AriaRole.Link, new() { Name = "Next" }).ClickAsync();
await page.WaitForURLAsync($"**/page/{i + 2}/");
The GetByRole method finds the link by its accessible role and
name, the way a user perceives the page. After the click, we wait until the
browser navigates to the next page. The URL pattern is a glob, where
** matches any characters.
$ dotnet run https://quotes.toscrape.com/page/2/ - first author: Marilyn Monroe https://quotes.toscrape.com/page/3/ - first author: Pablo Neruda https://quotes.toscrape.com/page/4/ - first author: Dr. Seuss
C# Playwright fill form
Playwright can fill in and submit forms. The login form of the quotes website accepts any user name and password.
using Microsoft.Playwright;
using var pw = await Playwright.CreateAsync();
await using var browser = await pw.Chromium.LaunchAsync();
var page = await browser.NewPageAsync();
await page.GotoAsync("https://quotes.toscrape.com/login");
await page.Locator("#username").FillAsync("john");
await page.Locator("#password").FillAsync("secret");
await page.GetByRole(AriaRole.Button, new() { Name = "Login" }).ClickAsync();
await page.GetByRole(AriaRole.Link, new() { Name = "Logout" }).WaitForAsync();
Console.WriteLine(page.Url);
Console.WriteLine("logged in");
We log in to the website.
await page.Locator("#username").FillAsync("john");
await page.Locator("#password").FillAsync("secret");
await page.GetByRole(AriaRole.Button, new() { Name = "Login" }).ClickAsync();
The FillAsync method types the text into the input fields, which
we find by their ids. Then we click the Login button.
await page.GetByRole(AriaRole.Link, new() { Name = "Logout" }).WaitForAsync();
After a successful login, the page shows a Logout link. We wait for it to appear.
$ dotnet run https://quotes.toscrape.com/ logged in
C# Playwright monitor requests and responses
The Request event is raised when a page issues a request. The
Response event is raised when a response is received for a
request.
using Microsoft.Playwright;
using var pw = await Playwright.CreateAsync();
await using var browser = await pw.Chromium.LaunchAsync();
var page = await browser.NewPageAsync();
page.Request += (_, req) => Console.WriteLine($">> {req.Method} {req.Url}");
page.Response += (_, resp) => Console.WriteLine($"<< {resp.Status} {resp.Url}");
await page.GotoAsync("https://quotes.toscrape.com");
We log a message for each request and response. Besides the page itself, the browser loads its stylesheets and fonts.
$ dotnet run >> GET https://quotes.toscrape.com/ << 200 https://quotes.toscrape.com/ >> GET https://quotes.toscrape.com/static/bootstrap.min.css >> GET https://quotes.toscrape.com/static/main.css << 200 https://quotes.toscrape.com/static/main.css << 200 https://quotes.toscrape.com/static/bootstrap.min.css >> GET https://fonts.googleapis.com/css?family=Raleway:400,700 << 200 https://fonts.googleapis.com/css?family=Raleway:400,700
The requests are made in parallel, so the responses can arrive in a different order.
Source
In this article we have used the Playwright library to automate browsers in C#.
Author
List all C# tutorials.