Skip to content

Build Typescript Project with Bazel Chapter 1: Bazel introduction

Bazel is a fast, scalable, incremental, and universal (for any languages/frameworks) build tool, and is especially useful for big mono repo projects.

I would like to write a series of blogs to introduce the concept of how to build a typescript project with Bazel.

  • Chapter 1: Bazel Introduction
  • Chapter 2: Bazel file structure and Bazel Query
  • Chapter 3: Build/Develop/Test a typescript project

In chapter 1, I would like introduce basic Bazel concepts, and define some of the benefits we can expect from using Bazel.

  • What is Bazel?
  • Bazel: Correctness
  • Bazel: Fast
  • Bazel: Universal
  • Bazel: Industrial grade

What is Bazel?

As you may know, we already have a lot of build tools. They include:

  • CI tools: Jenkins/CircleCI
  • Compile tools: tsc/sass
  • Bundle tools: webpack/rollup
  • Coordinate tools: make/grunt/gulp

So what is Bazel? Does it simply replace Jenkins or Webpack? @AlexEagle helped us answer this question at ngconf 2019, but here is a great picture that will explain a little as well. Build tools

So Bazel is a build tool, used to coordinate other tools (compile/bundle tools), and will use all the existing tools (such as tsc/webpack/rollup) to do the underlying work.

Another graph, also from @AlexEagle, will show this relationship more clearly. Bazel is a Hub

Ok, so Bazel is at the same position as Gulp, why not continue to use Gulp?

To answer this question, let's think about what the goal of the build tool is.

  • Essential:
    • Correct - don't need to worry about environmental pollution.
    • Fast
      • Incremental
      • Parallelism
    • Predictable - Same input will guarantee the same output.
    • Reusable - Build logic can be easily composed and reused.
  • Nice to have:
    • Universal - support multiple languages and frameworks.


This is the most important requirement. We all want our build systems to be stable, and we don't want them to generate unexpected results. Therefore, we want every build to be executed in an isolated environment. Otherwise, we will run into problems if, for example, we forget to delete some temp files, forget to reset environment variables, or if the build only works under certain conditions.

  • Sandboxing: Bazel supports sandboxing to isolate the environment. When we do a Bazel build, it will create an isolated working folder, and Bazel will run the build process under this folder. This is what we would call "sandboxing". Bazel will then restrict the access to the files outside of this folder. Also, Bazel makes sure that elements of the build tool, such as the compiler, only know their own input files, so the output will only depend on input. xy087mf2gsrbfdbe3t4r

  • The rule can only access an input file. Unlike a Gulp task, a Bazel rule can only access the files declared as input (we will talk about the target/rule in detail later).

Here is an example of a Gulp task:

gulp.task('compile', ['depTask'], () => {
  // do compile

So inside of a Gulp task, there is just a normal function. There is no concept of Input, and dependencies only tells gulp to run tasks in a specified order, so the task can access any files, and use any environmental variables with no restrictions. Gulp will have no idea which files are used in this task, so if some logic depends on the unintended file access/environment reference, it is impossible for Gulp to guarantee that the task will always generate the same results.

Let's see a Bazel target.

    name = "compile",
    srcs = ["a.ts"],

We will talk about the Bazel target/rule in more detail in the next chapter. Here, we will declare a Bazel target with a ts_library rule. Unlike with a Gulp task, here we have a strict input which is srcs = ["a.ts"], so when Bazel decides to execute this target, the typescript compiler can only access the file a.ts inside of the sandbox, and nowhere else. Therefore, there is no way that the Bazel target will produce wrong results because of the unpredictable environment or input.


Bazel is incremental because Bazel is declarative and predictable.

Bazel is Declarative

Let's use Gulp to compile those two files, in order to demonstrate that Gulp is imperative and Bazel is declarative. Let's see an example with Gulp.

// gulpfile.js
gulp.task('compile', () => {
  gulp.src(['user.ts', 'print.ts'])

gulp.task('test', ['compile'], () => {
   // run test depends on compile task

When we run gulp test for the first time, both the compile, and the test tasks will be executed. And then, even if we don't change any files, those two tasks will still be executed if we run gulp test again. Gulp is imperative, so we just have to tell it to do those two commands, and Gulp will do what we asked. Specifically, it checks the dependency, and guarantees the execution order. That's all.

Let's see how Bazel works. Here, we have two typescript files: user.ts, and print.ts. print.ts uses user.ts.

// user.ts
export class User {
  constructor(public name: string) {}

  toString() {
    return `user: ${}`;
// print.ts
import {User} from './user';

function printUser(user: User) {
  console.log(`the user is ${}`);

printUser(new User('testUser'));

To demonstrate that Bazel is declarative, let's use two Bazel build targets.

# src/BUILD.bazel
    name = "user",
    srcs = ["user.ts"],

    name = "print",
    srcs = ["print.ts"],
    deps = [":user"]

So we declare two Bazel targets, user, and print. The print target depends on the user target. All those targets are using the ts_library rule. It contains the metadata to tell Bazel how to compile the typescript files. And again, all this information is just a definition. It's not about commands, so when you use Bazel to build those targets, it is up to Bazel to decide whether to execute those rules or not.

Let's see the result first.

When we run bazel build //src:print, both the user and print targets will be compiled, which makes sense. When we run bazel build //src:print again, you will find Bazel will not run any targets because nothing changed, and Bazel knows it. As a result, Bazel decides not to run any targets.

Let's change something in user.ts, and see what happens.

// updated user.ts
export class User {
  constructor(public name: string) {}

  toString() {
    return `updated toString of user: ${}`;

After we run bazel build //src:print again, we may expect that both user and print will be compiled once more because user.ts has been changed, and print.ts references user.ts, and the print target depends on the user target. But the result is that only the user target has been compiled, and the print target has not. Why?

This is because changes in user.ts don't impact print.ts, and Bazel understands this.

Let's check out the following graph, which describes the input/output of the target. Target input/output

So for user target, the input is user.ts, and we have two outputs. One is user.js, and the other is user.d.ts. The latter of the two is the typescript declaration file. So let's see the relationship between the user, and print target.

Target dependency

Here, we can see that the print target depends on the user target, and that it uses one of the user target's outputs, user.d.ts, as it's own input. So, because we only updated toString of user.ts, and the user.d.ts was not changed at all, Bazel analyses the dependency graph. As a result, it knows that only user target needs to be built. Further, it also knows that the print target doesn't need to be built because the inputs of the print target, which are user.d.ts and print.ts, have not changed. Because of this, Bazel decides not to build print target.

It is very important to remember that Bazel is declarative and not imperative.

Dependency Graph

Bazel analyses the input/output of all build targets to determine which targets need to be executed.

(We can generate the dependency graph with bazel query, and we will talk about it in the next chapter.)

So Bazel can do incremental builds based on the analysis of the dependency graph, and only build the really impacted targets.

Bazel is Predictable (Bazel's Rules are Pure Functions)

Also, all Bazel rules are pure functions, so the same input will always result in the same output, hence Bazel is predictable. We can use the input as a key to cache the result of each target, and save the cache locally or remotely.

Remote cache

For example, developer 1 builds some targets, and pushes the result to remote cache. The other developer can then directly use this cache without building those targets in their own environment.

So these amazing features make Bazel super fast.


Bazel is a coordinate tool. It doesn't rely on any specified languages/frameworks, and it can build within almost all languages/frameworks from server to client, and from desktop to mobile.

It is difficult and costly to employ a specialist team to handle builds with several build tools/frameworks when working on a full-stack project. In one of my previous projects, we used Maven to build a Java backend, used Webpack to build its frontend, and used XCode and Gradle to build iOS and Android clients. Consequently, we needed a special build team consisting of people that knew all of those build tools, which makes it very difficult to do an incremental build, cache the results, or share the build script with other projects.

Bazel is also a perfect tool for mono repo, and full-stack projects that include multiple languages/frameworks.

Industrial Grade

Bazel is not an experimental project. It is used in almost all projects inside Google. When I started contributing to Angular, I did not use Bazel. Because of this, the time that Angular CI was taking was about 1 hour. Once Bazel was introduced to Angular CI, the time reduced to about 15 minutes, and the build process became much more stable, and less flaky than before, even with double the amount of test cases. It is amazing! I believe that Bazel will be the "must have" tool for many big projects.

I really like Bazel, and in the next blog post, I would like to introduce Bazel's file structure with bazel query.

Thanks for reading, and any feedback is appreciated.

This Dot Labs is a development consultancy that is trusted by top industry companies, including Stripe, Xero, Wikimedia, Docusign, and Twilio. This Dot takes a hands-on approach by providing tailored development strategies to help you approach your most pressing challenges with clarity and confidence. Whether it's bridging the gap between business and technology or modernizing legacy systems, you’ll find a breadth of experience and knowledge you need. Check out how This Dot Labs can empower your tech journey.

You might also like

Build Typescript Project with Bazel Chapter 2: File Structure cover image

Build Typescript Project with Bazel Chapter 2: File Structure

Build Typescript Project with Bazel Chapter 2: File Structure In the last chapter, we introduced the basic concept of Bazel. In this blog, I would like to talk about the file structure of Bazel. Concept and Terminology Before we introduce the file structure, we need to understand several key concepts and terminology in Bazel. - Workspace - Package - Target - Rule These concepts, and terminology, are composed to Build File`, which Bazel will analyze, and execute. The basic relationship among these concepts looks like this , we will discuss the details one by one. Workspace A "workspace" refers to the directories, which contain 1. The source files of the project. 2. Symbolic links contain the build output. And the Bazel definition is in a file named WORKSPACE`, or `WORKSPACE.bazel` at the root of the project directory. NOTE, one project can only have one `WORKSPACE` definition file. Here is an example of the WORKSPACE` file. ` workspace( name = "comthisdot_bazel_demo", ) load("@bazeltools//tools/build_defs/repo:http.bzl", "http_archive") Fetch rules_nodejs so we can install our npm dependencies httparchive( name = "buildbazel_rules_nodejs", sha256 = "ad4be2c6f40f5af70c7edf294955f9d9a0222c8e2756109731b25f79ea2ccea0", urls = [""], ) load("@buildbazel_rules_nodejs//:defs.bzl", "node_repositories", "yarn_install") noderepositories() yarninstall( name = "npm", packagejson = "//:package.json", yarnlock = "//:yarn.lock", ) Install all Bazel dependencies of the @npm npm packages load("@npm//:installbazel_dependencies.bzl", "install_bazel_dependencies") installbazel_dependencies() Setup the rules_typescript toolchain load("@npmbazel_typescript//:index.bzl", "ts_setup_workspace") tssetup_workspace() ` In a WORKSPACE` file, we should 1. Define the name of the workspace. The name should be unique globally, or at least unique in your organization. You could use the reverse dns name, such as com_thisdot_bazel_demo`, or the name of the project on GitHub. 2. Install environment related packages, such as yarn/npm/bazel`. 3. Setup toolchains needed to build/test` the project, such as `typescript/karma`. Once WORKSPACE` is ready, application developers don't really need to touch this file. Package - The primary unit of code organization (something like module) in a repository - Collection of related files and a specification of the dependencies among them - Directory containing a file named BUILD or BUILD.bazel, residing beneath the top-level directory in the workspace - A package includes all files in its directory, plus all subdirectories beneath it, except those which, themselves, contain a BUILD file It is important to know how to split a project into package`. It should be easy for the users to develop/test/share the unit of a `package`. If the unit is too big, the `package` has to be rebuilt on every package file change. If the unit is too small, it will be very hard to maintain and share. So, this is not an issue of `Bazel`. It is a general problem of project management. In Bazel, every package` will have a `BUILD.bazel` file, containing all of the `build`/`test`/`bundle` target definitions. For example, here is a of the Angular structure. Every directory under packages directory is a `package` of code organization, and also the build organization of Bazel. Let's take a look at the file structure of gulpjs` in Angular, so we can have a better understanding about the difference between Bazel and gulpjs. ` gulp.task('build-animations', () => {});; gulp.task('build-core', () => {}); gulp.task('build-core-schematics', () => {}); ` In most cases, - a gulpjs file doesn't have 1:1 relationship to the package directory. - a gulpjs file can reference any files inside the project. But for Bazel, - Each package` should have their own `BUILD.bazel` file. - The BUILD.bazel` can only reference the file inside the current `package`, and if the current package depends on other packages, we need to reference the Bazel build `target` from the other packages instead of the files directly. Here is a Bazel Package directory structure in Angular repo. Build File Before we talk about target`, let's take a look at the content of a `BUILD.bazel` file. ` package(defaultvisibility = ["//visibility:private"]) load("@npmbazel_typescript//:index.bzl", "ts_library") tslibrary( name = "lib", srcs = [":lib.ts"], visibility = ["//visibility:public"], ) ` The language of the BUILD.bazel` file is `Starlark`. - Starlark` is a subset of `Python`. - It is a very feature-limited language. A ton of Python` features, such as `class`, `import`, `while`, `yield`, `lambda`, `is`, `raise`, are not supported. - Recursion` is not allowed. - Most of Python's builtin methods are not supported. So Starlark` is a very very simple language, and only supports very limited `Python` syntax. Target The BUILD.bazel` file contains build targets. Those `target`s are the definitions of the `build`, `test`, and `bundle` work we want to achieve. The build target can represent: - Files - Rules The target can also depend on other targets - Circular dependencies are not allowed - Two targets, generating the same output, will cause a problem - Target dependency must be declared explicitly. Let's see the previous sample, ` package(defaultvisibility = ["//visibility:private"]) load("@npmbazel_typescript//:index.bzl", "ts_library") tslibrary( name = "lib", srcs = [":lib.ts"], visibility = ["//visibility:public"], ) ` Here, ts_library` is a rule imported from `@npm_bazel_typescript` workspace, and `ts_library(name = "lib")` is a `target`. The name is `lib`, and this target defines the metadata for compiling the `lib.ts` with `ts_library` rule. Label Every target has a unique name called label`. For example, if the `BUILD.bazel` file above is under `/lib` directory, then the label of the target is ` @comthisdot_bazel_demo//lib:lib ` The label is composed of several parts. 1. the name of the workspace: @com_thisdot_bazel_demo`. 2. the name of the package: lib`. 3. the name of the target: lib`. So, the composition is //:`. Most of the times, the name of the workspace can be omitted, so the label above can also be expressed as //lib:lib`. Additionally, if the name of the target is the same as the package's name, the name of the target can also be omitted. Therefore, the label above can also be expressed as //lib`. NOTE: The label for the target needs to be unique in the workspace. Visibility We can also define the visibility to define whether the rule inside this package can be used by other packages. ` package(defaultvisibility = ["//visibility:private"]) ` The visibility can be: - private`: the rules can be only used inside the current package. - public`: the rules can be used everywhere. - //some_package:package_scope`: the rules can only be used in the specified scope under `//some_package`. The `package_scope` can be: `__pkg__`/`__subpackages__`/`package group`. And if the rules in one package can be accessed from the other package, we can use load` to import them. For example: ` load("@npmbazel_typescript//:index.bzl", "ts_library") ` Here, we import the ts_library` rule from the Bazel typescript package. Target - Target can be Files` or `Rule`. - Target has input` and `output`. The `input` and `output` are known at build time. - Target will only be rebuilt when input` changes. Let's take a look at Rule` first. Rule The rule is just like a function` or `macro`. It can accept `named parameters` as options. Just like in the previous post, calling a rule will not execute an action. It is just metadata. Bazel will decide what to do. ` tslibrary( name = "lib", srcs = [":lib.ts"], visibility = ["//visibility:public"], ) ` So here, we use the ts_library` rule to define a target, and the name is `lib`. The srcs is `lib.ts` in the same directory. The visibility is `public`, so this target can be accessed from the other packages. Rule Naming It is very important to follow the naming convention when you want to create your own rule. - *_binary`: executable programs in a given language (nodejs_binary) - *_test`: special _binary rule for testing - *_library`: compiled module for a given language (ts_library) Rule common attributes Several common attributes exist in almost all rules. For example: ` tslibrary( name = "lib", srcs = [":index.ts"], tags = ["build-target"], visibility = ["//visibility:public"], deps = [ ":date", ":user", ], ) ` - name: unique name within this package - srcs: inputs of the target, typically files - deps: compile-time dependencies - data: runtime dependencies - testonly: target which should be executed only when running Bazel test - visibility: specifies who can make a dependency on the given target Let's see another example: ` httpserver( name = "prodserver", data = [ "index.html", ":bundle", "styles.css", ], ) ` Here, we use the data` attribute. The `data` will only be used at runtime. It will not be analyzed by Bazel at build time. So, in this blog, we introduced the basic Bazel structure concepts. In the next blog, we will introduce how to query Bazel targets....

Angular 11 Released cover image

Angular 11 Released

I'm so excited to see the recent release of Angular v11.0.0! There are lot of improvements especially with regards to tooling. What's new in the release CLI: Inlining of Google Fonts First, contentful paint is always the critical part for performance. [Eliminate render-blocking resources] ( is an important guide to improve the FCP performance, and Angular is also working on following the guide to inline the blocking resources. In Angular v11, the Google Fonts will be inline by adding the following options to the angular.json`. ` "optimization": { "fonts": true } ` And without this option, your Google font importing looks like this. ` ` After activating this optimization, it looks like this. ` @font-face { font-family: 'Material Icons'; font-style: normal; font-weight: 400; src: url( format('woff2'); } .material-icons { font-family: 'Material Icons'; font-weight: normal; font-style: normal; font-size: 24px; line-height: 1; letter-spacing: normal; text-transform: none; display: inline-block; white-space: nowrap; word-wrap: normal; direction: ltr; } ` CLI: Hot Module Replacement Support Angular supports HMR` for a while, but to make it work requires configuration and code changes, which is not a good developer experience. From v11, it is simple to enable the `HMR` support by using ` ng serve --hmr ` Now, the live reload will also keep the state of your application, so it is much easier to apply some small updates to your application. CLI: Experimental Webpack 5 support Webpack 5 supports many new features such as: - Module federation which is a new way to develop micro-frontend - Disk caching for faster build - Better tree shaking Now, you can opt-in the Webpack 5 by adding the following section in the package.json file. ` "resolutions": { "webpack": "5.4.0" } ` CLI: ng build better output Now, running ng build` will generate more clear output. CLI: ng serve ask for new port It is a really nice feature! If the 4200` port is in use, `ng serve` will ask you for a different port without re-run with --port option. ` ? Port 4200 is already in use. Would you like to use a different port? (Y/n) Angular Live Development Server is listening on localhost:60335, open your browser on http://localhost:60335/ ** ` CLI: Lint Now, Angular uses TSLint as the default linter, and since TSLint is deprecated now, Angular uses angular-eslint` to replace `TSLint/Codelyzer`. Here is the migration guide. Features: Trusted Types support Trusted Types is a new solution to prevent DOM-based cross-site scripting vulnerabilities. Consider the following example: ` anElement.innerHTML = location.href; ` With Trusted Types enabled, the browser throws a TypeError and prevent this behavior. To tell the browser this is a trusted safe operation, you need to do it like this: ` anElement.innerHTML = aTrustedHTML; ` Note that the aTrustedHtml` is a `TrustedHtml` object. It looks similar to the DomSanitizer's functionality, so now Angular permits support to allow such an operation to return a Trusted Types. Tooling - Typescript is updated to v4.0 Deprecations - IE 9,10 and IE mobile are no longer supported. - ViewEncapsulation.Native has been removed. - async` test helper function is renamed to `waitForAsync`. For the full deprecation list, please check here. Triaging issues and PRs The Angular team put forward a significant effort to go through all the issues/PRs now opened in the GitHub, and check/triage these issues to make sure that they are at the correct state. It greatly helps the community to get better/faster support and feedback. I am so honored to also take part in this process. How to update to v11 It is very simple. Just use the Angular CLI. ` ng update @angular/cli @angular/core ` You can find more information from and also from the official blog....

I Broke My Hand So You Don't Have To (First-Hand Accessibility Insights) cover image

I Broke My Hand So You Don't Have To (First-Hand Accessibility Insights)

We take accessibility quite seriously here at This Dot because we know it's important. Still, throughout my career, I've seen many projects where accessibility was brushed aside for reasons like "our users don't really use keyboard shortcuts" or "we need to ship fast; we can add accessibility later." The truth is, that "later" often means "never." And it turns out, anyone could break their hand, like I did. I broke my dominant hand and spent four weeks in a cast, effectively rendering it useless and forcing me to work left-handed. I must thus apologize for the misleading title; this post should more accurately be dubbed "second-hand" accessibility insights. The Perspective of a Developer Firstly, it's not the end of the world. I adapted quickly to my temporary disability, which was, for the most part, a minor inconvenience. I had to type with one hand, obviously slower than my usual pace, but isn't a significant part of a software engineer's work focused on thinking? Here's what I did and learned: - I moved my mouse to the left and started using it with my left hand. I adapted quickly, but the experience wasn't as smooth as using my right hand. I could perform most tasks, but I needed to be more careful and precise. - Many actions require holding a key while pressing a mouse button (e.g., visiting links from the IDE), which is hard to do with one hand. - This led me to explore trackpad options. Apart from the Apple Magic Trackpad, choices were limited. As a Windows user (I know, sorry), that wasn't an option for me. I settled for a cheap trackpad from Amazon. A lot of tasks became easier; however, the trackpad eventually malfunctioned, sending me back to the mouse. - I don't know a lot of IDE shortcuts. I realized how much I've been relying on a mouse for my work, subconsciously refusing to learn new keyboard shortcuts (I'll be returning my senior engineer license shortly). So I learned a few new ones, which is good, I guess. - Some keyboard shortcuts are hard to press with one hand. If you find yourself in a similar situation, you may need to remap some of them. - Copilot became my best friend, saving me from a lot of slow typing, although I did have to correct and rewrite many of its suggestions. The Perspective of a User As a developer, I was able to get by and figure things out to be able to work effectively. As a user, however, I got to experience the other side of the coin and really feel the accessibility (or lack thereof) on the web. Here are a few insights I gained: - A lot of websites apparently tried_ to implement keyboard navigation, but failed miserably. For example, a big e-commerce website I tried to use to shop for the aforementioned trackpad seemed to work fine with keyboard navigation at first, but once I focused on the search field, I found myself unable to tab out from it. When you make the effort to implement keyboard navigation, please make sure it works properly and it doesn't get broken with new changes. I wholeheartedly recommend having e2e tests (e.g. with Playwright) that verify the keyboard navigation works as expected. - A few websites and web apps I tried to use were completely unusable with the keyboard and were designed to be used with a mouse only. - Some sites had elaborate keyboard navigation, with custom keyboard shortcuts for different functionality. That took some time to figure out, and I reckon it's not as intuitive as the designers thought it would be. Once a user learns the shortcuts, however, it could make their life easier, I suppose. - A lot of interactive elements are much smaller than they should be, making it hard to accurately click on them with your weaker hand. Designers, I beg you, please make your buttons bigger. I once worked on an application that had a "gloves mode" for environments where the operators would be using gloves, and I feel like maybe the size we went with for the "gloves mode" should be the standard everywhere, especially as screens get bigger and bigger. - Misclicking is easy, especially using your weaker hand. Be it a mouse click or just hitting an Enter key on accident. Kudos to all the developers who thought about this and implemented a confirmation dialog or other safety measures to prevent users from accidentally deleting or posting something. I've however encountered a few apps that didn't have any of these, and those made me a bit anxious, to be honest. If this is something you haven't thought about when developing an app, please start doing so, you might save someone a lot of trouble. Some Second-Hand Insights I was only a little bit impaired by being temporarily one-handed and it was honestly a big pain. In this post, I've focused on my anecdotal experience as a developer and a user, covering mostly keyboard navigation and mouse usage. I can only imagine how frustrating it must be for visually impaired users, or users with other disabilities, to use the web. I must confess I haven't always been treating accessibility as a priority, but I've certainly learned my lesson. I will try to make sure all the apps I work on are accessible and inclusive, and I will try to test not only the keyboard navigation, ARIA attributes, and other accessibility features, but also the overall experience of using the app with a screen reader. I hope this post will at least plant a little seed in your head that makes you think about what it feels like to be disabled and what would the experience of a disabled person be like using the app you're working on. Conclusion: The Humbling Realities of Accessibility The past few weeks have been an eye-opening journey for me into the world of accessibility, exposing its importance not just in theory but in palpable, daily experiences. My short-term impairment allowed me to peek into a life where simple tasks aren't so simple, and convenient shortcuts are a maze of complications. It has been a humbling experience, but also an illuminating one. As developers and designers, we often get caught in the rush to innovate and to ship, leaving behind essential elements that make technology inclusive and humane. While my temporary disability was an inconvenience, it's permanent for many others. A broken hand made me realize how broken our approach towards accessibility often is. The key takeaway here isn't just a list of accessibility tips; it's an earnest appeal to empathize with your end-users. "Designing for all" is not a checkbox to tick off before a product launch; it's an ongoing commitment to the understanding that everyone interacts with technology differently. When being empathetic and sincerely thinking about accessibility, you never know whose life you could be making easier. After all, disability isn't a special condition; it's a part of the human condition. And if you still think "Our users don't really use keyboard shortcuts" or "We can add accessibility later," remember that you're not just failing a compliance checklist, you're failing real people....