diff --git a/examples/custom-rules/README.md b/examples/custom-rules/README.md
new file mode 100644
index 000000000..e681a6841
--- /dev/null
+++ b/examples/custom-rules/README.md
@@ -0,0 +1,65 @@
+# Custom Rules Examples
+
+This directory contains example custom rules for HTMLHint to demonstrate how to create and use custom rules.
+
+## Example Rule
+
+The `example-rule.js` file demonstrates a custom rule that:
+
+1. Checks if images have either a `title` or `alt` attribute for accessibility
+2. Validates class names against a configurable pattern
+
+## Usage
+
+### Loading the Example Rule
+
+```shell
+# Load the example rule
+npx htmlhint --rulesdir ./examples/custom-rules/example-rule.js test.html
+```
+
+### Configuration
+
+You can configure the example rule in your `.htmlhintrc` file:
+
+```json
+{
+ "example-rule": {
+ "classPattern": "^[a-z][a-z0-9-]*$"
+ }
+}
+```
+
+Or via command line:
+
+```shell
+npx htmlhint --rulesdir ./examples/custom-rules/ --rules "example-rule:{classPattern:'^[a-z][a-z0-9-]*$'}" test.html
+```
+
+### Testing
+
+Create a test HTML file to see the rule in action:
+
+```html
+
+
+
+ Test
+
+
+
+
+
+
+ Content
+
+
+
+ Content
+
+
+```
+
+## Creating Your Own Rules
+
+See the [Custom Rules documentation](https://htmlhint.com/docs/usage/custom-rules/) for detailed information on creating custom rules.
diff --git a/examples/custom-rules/example-rule.js b/examples/custom-rules/example-rule.js
new file mode 100644
index 000000000..a7d081578
--- /dev/null
+++ b/examples/custom-rules/example-rule.js
@@ -0,0 +1,47 @@
+/**
+ * Example custom rule for HTMLHint
+ * This rule demonstrates how to create a custom rule that checks for specific patterns
+ */
+
+module.exports = function (HTMLHint) {
+ HTMLHint.addRule({
+ id: 'example-rule',
+ description: 'Example custom rule that demonstrates custom rule creation',
+ init: function (parser, reporter, options) {
+ // Listen for start tags - Note: Use arrow functions for event listeners
+ parser.addListener('tagstart', (event) => {
+ const tagName = event.tagName.toLowerCase()
+ const mapAttrs = parser.getMapAttrs(event.attrs)
+
+ // Example: Check if elements have a title attribute when they should
+ if (tagName === 'img' && !mapAttrs.title && !mapAttrs.alt) {
+ reporter.warn(
+ 'Images should have either a title or alt attribute for accessibility',
+ event.line,
+ event.col,
+ this,
+ event.raw
+ )
+ }
+
+ // Example: Check for specific class naming convention
+ if (mapAttrs.class && options && options.classPattern) {
+ const classPattern = new RegExp(options.classPattern)
+ const classes = mapAttrs.class.split(/\s+/)
+
+ for (const className of classes) {
+ if (!classPattern.test(className)) {
+ reporter.warn(
+ `Class "${className}" does not match the required pattern: ${options.classPattern}`,
+ event.line,
+ event.col,
+ this,
+ event.raw
+ )
+ }
+ }
+ }
+ })
+ },
+ })
+}
diff --git a/website/src/content/docs/usage/cli.md b/website/src/content/docs/usage/cli.md
index 9a099d2f1..dda5a921e 100644
--- a/website/src/content/docs/usage/cli.md
+++ b/website/src/content/docs/usage/cli.md
@@ -40,7 +40,15 @@ Set all of the rules available
### `--rulesdir, -R`
-Load custom rules from file or folder
+Load custom rules from file or folder. See the [custom rules documentation](/usage/custom-rules/) for detailed information on creating and using custom rules.
+
+```shell
+# Load a single custom rule file
+npx htmlhint --rulesdir ./my-custom-rule.js index.html
+
+# Load all custom rules from a directory
+npx htmlhint --rulesdir ./custom-rules/ index.html
+```
### `--version, -V`
diff --git a/website/src/content/docs/usage/custom-rules.mdx b/website/src/content/docs/usage/custom-rules.mdx
new file mode 100644
index 000000000..ec4956580
--- /dev/null
+++ b/website/src/content/docs/usage/custom-rules.mdx
@@ -0,0 +1,269 @@
+---
+id: custom-rules
+title: Custom Rules
+description: Learn how to create and use custom rules to extend HTMLHint's functionality for your specific needs.
+sidebar:
+ order: 4
+---
+
+# Custom Rules
+
+HTMLHint allows you to create custom rules to extend its functionality for your specific needs. Custom rules can be loaded using the `--rulesdir` option and follow the same pattern as built-in rules.
+
+## Creating Custom Rules
+
+Custom rules are JavaScript modules that export a function. The function receives the `HTMLHint` instance as a parameter and should register the custom rule using `HTMLHint.addRule()`.
+
+### Basic Custom Rule Structure
+
+```javascript
+// my-custom-rule.js
+module.exports = function(HTMLHint) {
+ HTMLHint.addRule({
+ id: 'my-custom-rule',
+ description: 'This is my custom rule description',
+ init: function(parser, reporter, options) {
+ // Rule implementation goes here
+ }
+ });
+};
+```
+
+### Rule Object Properties
+
+Each custom rule should have the following properties:
+
+- **`id`** (string): Unique identifier for the rule. This is used in configuration files and command line options.
+- **`description`** (string): Human-readable description of what the rule does. This appears in the `--list` output.
+- **`init`** (function): Function that initializes the rule with the parser and reporter.
+
+### The `init` Function
+
+The `init` function is where your rule logic goes. It receives three parameters:
+
+- **`parser`**: The HTML parser instance that provides events as it parses the HTML
+- **`reporter`**: The reporter instance for generating warnings, errors, or info messages
+- **`options`**: Rule-specific options from the configuration
+
+### Important: Using Arrow Functions
+
+When adding event listeners, **always use arrow functions** instead of function expressions to ensure the correct `this` context is maintained when calling reporter methods:
+
+```javascript
+// ✅ Correct - Arrow function preserves 'this' context
+parser.addListener('tagstart', (event) => {
+ reporter.warn('Message', event.line, event.col, this, event.raw);
+});
+
+// ❌ Incorrect - Function expression loses 'this' context
+parser.addListener('tagstart', function(event) {
+ reporter.warn('Message', event.line, event.col, this, event.raw); // 'this' is parser, not rule
+});
+```
+
+## Example Custom Rules
+
+### Example 1: Simple Tag Check
+
+This rule warns when a specific tag is used:
+
+```javascript
+// no-div-tags.js
+module.exports = function(HTMLHint) {
+ HTMLHint.addRule({
+ id: 'no-div-tags',
+ description: 'Div tags are not allowed',
+ init: function(parser, reporter, options) {
+ parser.addListener('tagstart', (event) => {
+ const tagName = event.tagName.toLowerCase();
+
+ if (tagName === 'div') {
+ reporter.warn(
+ 'Div tags are not allowed. Use semantic HTML elements instead.',
+ event.line,
+ event.col,
+ this,
+ event.raw
+ );
+ }
+ });
+ }
+ });
+};
+```
+
+### Example 2: Attribute Validation
+
+This rule checks for specific attribute patterns:
+
+```javascript
+// data-attributes-required.js
+module.exports = function(HTMLHint) {
+ HTMLHint.addRule({
+ id: 'data-attributes-required',
+ description: 'Elements with class "component" must have a data-component attribute',
+ init: function(parser, reporter, options) {
+ parser.addListener('tagstart', (event) => {
+ const tagName = event.tagName.toLowerCase();
+ const mapAttrs = parser.getMapAttrs(event.attrs);
+
+ // Check if element has class "component"
+ if (mapAttrs.class && mapAttrs.class.includes('component')) {
+ // Check if it has data-component attribute
+ if (!mapAttrs['data-component']) {
+ reporter.warn(
+ 'Elements with class "component" must have a data-component attribute',
+ event.line,
+ event.col,
+ this,
+ event.raw
+ );
+ }
+ }
+ });
+ }
+ });
+};
+```
+
+### Example 3: Complex Validation with Options
+
+This rule accepts configuration options:
+
+```javascript
+// max-attributes.js
+module.exports = function(HTMLHint) {
+ HTMLHint.addRule({
+ id: 'max-attributes',
+ description: 'Elements should not have more than the specified number of attributes',
+ init: function(parser, reporter, options) {
+ const maxAttrs = options || 5; // Default to 5 if no options provided
+
+ parser.addListener('tagstart', (event) => {
+ const attrCount = event.attrs.length;
+
+ if (attrCount > maxAttrs) {
+ reporter.warn(
+ `Element has ${attrCount} attributes, but maximum allowed is ${maxAttrs}`,
+ event.line,
+ event.col,
+ this,
+ event.raw
+ );
+ }
+ });
+ }
+ });
+};
+```
+
+## Using Custom Rules
+
+### Loading Custom Rules
+
+Use the `--rulesdir` option to load custom rules:
+
+```shell
+# Load a single custom rule file
+npx htmlhint --rulesdir ./my-custom-rule.js index.html
+
+# Load all custom rules from a directory
+npx htmlhint --rulesdir ./custom-rules/ index.html
+```
+
+### Enabling Custom Rules
+
+After loading custom rules, you can enable them in several ways:
+
+#### In Configuration File
+
+```json
+{
+ "no-div-tags": true,
+ "data-attributes-required": true,
+ "max-attributes": 3
+}
+```
+
+#### Via Command Line
+
+```shell
+npx htmlhint --rulesdir ./custom-rules/ --rules no-div-tags,data-attributes-required,max-attributes:3 index.html
+```
+
+#### Inline in HTML
+
+```html
+
+
+
+ This will trigger warnings
+
+
+```
+
+## Parser Events
+
+The HTML parser provides several events you can listen to:
+
+- **`tagstart`**: Fired when a start tag is encountered
+- **`tagend`**: Fired when an end tag is encountered
+- **`text`**: Fired when text content is encountered
+- **`comment`**: Fired when a comment is encountered
+- **`cdata`**: Fired when CDATA is encountered
+- **`doctype`**: Fired when a DOCTYPE declaration is encountered
+
+### Event Object Properties
+
+Event objects typically contain:
+
+- **`tagName`**: The name of the tag
+- **`attrs`**: Array of attributes
+- **`line`**: Line number where the event occurred
+- **`col`**: Column number where the event occurred
+- **`raw`**: The raw HTML string for this element
+
+## Reporter Methods
+
+The reporter provides three methods for generating messages:
+
+- **`reporter.warn(message, line, col, rule, raw)`**: Creates a warning message
+- **`reporter.error(message, line, col, rule, raw)`**: Creates an error message
+- **`reporter.info(message, line, col, rule, raw)`**: Creates an info message
+
+## Best Practices
+
+1. **Use arrow functions**: Always use arrow functions for event listeners to preserve the correct `this` context
+2. **Use descriptive rule IDs**: Choose clear, descriptive names for your rules
+3. **Provide helpful error messages**: Make error messages actionable and informative
+4. **Handle options gracefully**: Always provide sensible defaults for rule options
+5. **Test your rules**: Create test cases to ensure your rules work correctly
+6. **Follow existing patterns**: Look at built-in rules for examples of good practices
+7. **Document your rules**: Include clear descriptions and examples
+
+## Directory Structure
+
+When organizing multiple custom rules, you can use a directory structure:
+
+```
+custom-rules/
+├── accessibility/
+│ ├── aria-required.js
+│ └── semantic-headings.js
+├── performance/
+│ ├── image-optimization.js
+│ └── script-loading.js
+└── style/
+ ├── class-naming.js
+ └── attribute-order.js
+```
+
+HTMLHint will recursively find all `.js` files in the specified directory and load them as custom rules.
+
+## Troubleshooting
+
+- **Rule not loading**: Check that your file exports a function and calls `HTMLHint.addRule()`
+- **Rule not running**: Ensure the rule is enabled in your configuration
+- **Parser errors**: Verify that you're using the correct event names and object properties
+- **Silent failures**: Custom rules that fail to load are silently ignored - check your JavaScript syntax
+- **Incorrect rule reporting**: Make sure you're using arrow functions for event listeners to preserve the `this` context
diff --git a/website/src/content/docs/usage/options.md b/website/src/content/docs/usage/options.md
index 3df6ab617..f7a253df8 100644
--- a/website/src/content/docs/usage/options.md
+++ b/website/src/content/docs/usage/options.md
@@ -38,3 +38,81 @@ Options are:
CLI flags: `--ignore, -i`
A list of patterns of files or folders to ignore. For example, `--ignore="**/folder/**,**/folder_two/**"`
+
+## `rulesdir`
+
+CLI flags: `--rulesdir, -R`
+
+Load custom rules from a file or directory. This allows you to extend HTMLHint with your own custom rules.
+
+### Usage
+
+```shell
+# Load a single custom rule file
+npx htmlhint --rulesdir ./my-custom-rule.js index.html
+
+# Load all custom rules from a directory (recursively finds all .js files)
+npx htmlhint --rulesdir ./custom-rules/ index.html
+```
+
+### Custom Rule Format
+
+Custom rules should be JavaScript modules that export a function. The function receives the `HTMLHint` instance as a parameter and should register the custom rule using `HTMLHint.addRule()`.
+
+#### Example Custom Rule
+
+```javascript
+// my-custom-rule.js
+module.exports = function(HTMLHint) {
+ HTMLHint.addRule({
+ id: 'my-custom-rule',
+ description: 'This is my custom rule description',
+ init: function(parser, reporter, options) {
+ // Rule implementation - Note: Use arrow functions for event listeners
+ parser.addListener('tagstart', (event) => {
+ const tagName = event.tagName.toLowerCase();
+
+ if (tagName === 'div') {
+ reporter.warn(
+ 'Custom rule: div tags are not allowed',
+ event.line,
+ event.col,
+ this,
+ event.raw
+ );
+ }
+ });
+ }
+ });
+};
+```
+
+**Important**: Always use arrow functions for event listeners to ensure the correct `this` context is maintained when calling reporter methods.
+
+#### Using Custom Rules
+
+After loading custom rules with `--rulesdir`, you can enable them in your configuration:
+
+```json
+{
+ "my-custom-rule": true
+}
+```
+
+Or via command line:
+
+```shell
+npx htmlhint --rulesdir ./custom-rules/ --rules my-custom-rule index.html
+```
+
+### Directory Loading
+
+When specifying a directory, HTMLHint will:
+
+1. Recursively search for all `.js` files in the directory
+2. Load each file as a custom rule module
+3. Skip any files that fail to load (errors are silently ignored)
+
+This makes it easy to organize multiple custom rules in a single directory structure.
+
+For detailed information on creating custom rules, see the [Custom Rules documentation](/usage/custom-rules/).