diff --git a/CHANGELOG.md b/CHANGELOG.md
index 503d8c6..a4cf968 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,8 @@
   - Highlight named lambda functions properly.
   - Fix syntax highlighting for functions and vars with metadata on the previous
     line.
+- Improve semantic indentation rules to be more consistent with cljfmt.
+- Introduce `clojure-ts-semantic-indent-rules` customization option.
 
 ## 0.2.3 (2025-03-04)
 
diff --git a/README.md b/README.md
index f4e53ef..4fb47b2 100644
--- a/README.md
+++ b/README.md
@@ -170,6 +170,45 @@ Set the var `clojure-ts-indent-style` to change it.
 >
 > You can find [this article](https://metaredux.com/posts/2020/12/06/semantic-clojure-formatting.html) comparing semantic and fixed indentation useful.
 
+#### Customizing semantic indentation
+
+The indentation of special forms and macros with bodies is controlled via
+`clojure-ts-semantic-indent-rules`. Nearly all special forms and built-in macros
+with bodies have special indentation settings in clojure-ts-mode, which are
+aligned with cljfmt indent rules. You can add/alter the indentation settings in
+your personal config. Let's assume you want to indent `->>` and `->` like this:
+
+```clojure
+(->> something
+  ala
+  bala
+  portokala)
+```
+
+You can do so by putting the following in your config:
+
+```emacs-lisp
+(setopt clojure-ts-semantic-indent-rules '(("->" . (:block 1))
+                                           ("->>" . (:block 1))))
+```
+
+This means that the body of the `->`/`->>` is after the first argument.
+
+The default set of rules is defined as
+`clojure-ts--semantic-indent-rules-defaults`, any rule can be overridden using
+customization option.
+
+There are 2 types of rules supported: `:block` and `:inner`, similarly to
+cljfmt. If rule is defined as `:block n`, `n` means a number of arguments after
+which begins the body. If rule is defined as `:inner n`, each form in the body
+is indented with 2 spaces regardless of `n` value (currently all default rules
+has 0 value).
+
+For example:
+- `do` has a rule `:block 0`.
+- `when` has a rule `:block 1`.
+- `defn` and `fn` have a rule `:inner 0`.
+
 ### Font Locking
 
 To highlight entire rich `comment` expression with the comment font face, set
diff --git a/clojure-ts-mode.el b/clojure-ts-mode.el
index 30d65cc..19fa605 100644
--- a/clojure-ts-mode.el
+++ b/clojure-ts-mode.el
@@ -125,6 +125,23 @@ double quotes on the third column."
   :type 'boolean
   :package-version '(clojure-ts-mode . "0.2.4"))
 
+(defcustom clojure-ts-semantic-indent-rules nil
+  "Custom rules to extend default indentation rules for `semantic' style.
+
+Each rule is an alist entry which looks like `(\"symbol-name\"
+. (rule-type rule-value))', where rule-type is one either `:block' or
+`:inner' and rule-value is an integer.  The semantic is similar to
+cljfmt indentation rules.
+
+Default set of rules is defined in
+`clojure-ts--semantic-indent-rules-defaults'."
+  :safe #'listp
+  :type '(alist :key-type string
+                :value-type (list (choice (const :tag "Block indentation rule" :block)
+                                          (const :tag "Inner indentation rule" :inner))
+                                  integer))
+  :package-version '(clojure-ts-mode . "0.2.4"))
+
 (defvar clojure-ts-mode-remappings
   '((clojure-mode . clojure-ts-mode)
     (clojurescript-mode . clojure-ts-clojurescript-mode)
@@ -182,7 +199,6 @@ Only intended for use at development time.")
     table)
   "Syntax table for `clojure-ts-mode'.")
 
-
 (defconst clojure-ts--builtin-dynamic-var-regexp
   (eval-and-compile
     (concat "^"
@@ -746,34 +762,135 @@ The possible values for this variable are
      ((parent-is "list_lit") parent 1)
      ((parent-is "set_lit") parent 2))))
 
-(defvar clojure-ts--symbols-with-body-expressions-regexp
-  (eval-and-compile
-    (rx (or
-         ;; Match def* symbols,
-         ;; we also explicitly do not match symbols beginning with
-         ;; "default" "deflate" and "defer", like cljfmt
-         (and line-start "def")
-         ;; Match with-* symbols
-         (and line-start "with-")
-         ;; Exact matches
-         (and line-start
-              (or "alt!" "alt!!" "are" "as->"
-                  "binding" "bound-fn"
-                  "case" "catch" "comment" "cond" "condp" "cond->" "cond->>"
-                  "delay" "do" "doseq" "dotimes" "doto"
-                  "extend" "extend-protocol" "extend-type"
-                  "fdef" "finally" "fn" "for" "future"
-                  "go" "go-loop"
-                  "if" "if-let" "if-not" "if-some"
-                  "let" "letfn" "locking" "loop"
-                  "match" "ns" "proxy" "reify" "struct-map"
-                  "testing" "thread" "try"
-                  "use-fixtures"
-                  "when" "when-first" "when-let" "when-not" "when-some" "while")
-              line-end))))
-  "A regex to match symbols that are functions/macros with a body argument.
-Taken from cljfmt:
-https://github.com/weavejester/cljfmt/blob/fb26b22f569724b05c93eb2502592dfc2de898c3/cljfmt/resources/cljfmt/indents/clojure.clj")
+(defvar clojure-ts--semantic-indent-rules-defaults
+  '(("alt!"            . (:block 0))
+    ("alt!!"           . (:block 0))
+    ("comment"         . (:block 0))
+    ("cond"            . (:block 0))
+    ("delay"           . (:block 0))
+    ("do"              . (:block 0))
+    ("finally"         . (:block 0))
+    ("future"          . (:block 0))
+    ("go"              . (:block 0))
+    ("thread"          . (:block 0))
+    ("try"             . (:block 0))
+    ("with-out-str"    . (:block 0))
+    ("defprotocol"     . (:block 1))
+    ("binding"         . (:block 1))
+    ("defprotocol"     . (:block 1))
+    ("binding"         . (:block 1))
+    ("case"            . (:block 1))
+    ("cond->"          . (:block 1))
+    ("cond->>"         . (:block 1))
+    ("doseq"           . (:block 1))
+    ("dotimes"         . (:block 1))
+    ("doto"            . (:block 1))
+    ("extend"          . (:block 1))
+    ("extend-protocol" . (:block 1))
+    ("extend-type"     . (:block 1))
+    ("for"             . (:block 1))
+    ("go-loop"         . (:block 1))
+    ("if"              . (:block 1))
+    ("if-let"          . (:block 1))
+    ("if-not"          . (:block 1))
+    ("if-some"         . (:block 1))
+    ("let"             . (:block 1))
+    ("letfn"           . (:block 1))
+    ("locking"         . (:block 1))
+    ("loop"            . (:block 1))
+    ("match"           . (:block 1))
+    ("ns"              . (:block 1))
+    ("struct-map"      . (:block 1))
+    ("testing"         . (:block 1))
+    ("when"            . (:block 1))
+    ("when-first"      . (:block 1))
+    ("when-let"        . (:block 1))
+    ("when-not"        . (:block 1))
+    ("when-some"       . (:block 1))
+    ("while"           . (:block 1))
+    ("with-local-vars" . (:block 1))
+    ("with-open"       . (:block 1))
+    ("with-precision"  . (:block 1))
+    ("with-redefs"     . (:block 1))
+    ("defrecord"       . (:block 2))
+    ("deftype"         . (:block 2))
+    ("are"             . (:block 2))
+    ("as->"            . (:block 2))
+    ("catch"           . (:block 2))
+    ("condp"           . (:block 2))
+    ("bound-fn"        . (:inner 0))
+    ("def"             . (:inner 0))
+    ("defmacro"        . (:inner 0))
+    ("defmethod"       . (:inner 0))
+    ("defmulti"        . (:inner 0))
+    ("defn"            . (:inner 0))
+    ("defn-"           . (:inner 0))
+    ("defonce"         . (:inner 0))
+    ("deftest"         . (:inner 0))
+    ("fdef"            . (:inner 0))
+    ("fn"              . (:inner 0))
+    ("reify"           . (:inner 0))
+    ("use-fixtures"    . (:inner 0)))
+  "Default semantic indentation rules.
+
+The format reflects cljfmt indentation rules.  All the default rules are
+aligned with
+https://github.com/weavejester/cljfmt/blob/0.13.0/cljfmt/resources/cljfmt/indents/clojure.clj")
+
+(defun clojure-ts--match-block-0-body (bol first-child)
+  "Match if expression body is not at the same line as FIRST-CHILD.
+
+If there is no body, check that BOL is not at the same line."
+  (let* ((body-pos (if-let* ((body (treesit-node-next-sibling first-child)))
+                       (treesit-node-start body)
+                     bol)))
+    (< (line-number-at-pos (treesit-node-start first-child))
+       (line-number-at-pos body-pos))))
+
+(defun clojure-ts--node-pos-match-block (node parent bol block)
+  "Return TRUE if NODE index in the PARENT matches requested BLOCK.
+
+NODE might be nil (when we insert an empty line for example), in this
+case we look for next available child node in the PARENT after BOL
+position.
+
+The first node in the expression is usually an opening paren, the last
+node is usually a closing paren (unless some automatic parens mode is
+not enabled).  If requested BLOCK is 1, the NODE index should be at
+least 3 (first node is opening paren, second node is matched symbol,
+third node is first argument, and the rest is body which should be
+indented.)"
+  (if node
+      (> (treesit-node-index node) (1+ block))
+    (when-let* ((node-after-bol (treesit-node-first-child-for-pos parent bol)))
+      (> (treesit-node-index node-after-bol) (1+ block)))))
+
+(defun clojure-ts--match-form-body (node parent bol)
+  "Match if NODE has to be indented as a for body.
+
+PARENT not should be a list.  If first symbol in the expression has an
+indentation rule in `clojure-ts--semantic-indent-rules-defaults' or
+`clojure-ts-semantic-indent-rules' check if NODE should be indented
+according to the rule.  If NODE is nil, use next node after BOL."
+  (and (clojure-ts--list-node-p parent)
+       (let ((first-child (clojure-ts--node-child-skip-metadata parent 0)))
+         (when-let* ((rule (alist-get (clojure-ts--named-node-text first-child)
+                                      (seq-union clojure-ts-semantic-indent-rules
+                                                 clojure-ts--semantic-indent-rules-defaults
+                                                 (lambda (e1 e2) (equal (car e1) (car e2))))
+                                      nil
+                                      nil
+                                      #'equal)))
+           (and (not (clojure-ts--match-with-metadata node))
+                (let ((rule-type (car rule))
+                      (rule-value (cadr rule)))
+                  (if (equal rule-type :block)
+                      (if (zerop rule-value)
+                          ;; Special treatment for block 0 rule.
+                          (clojure-ts--match-block-0-body bol first-child)
+                        (clojure-ts--node-pos-match-block node parent bol rule-value))
+                    ;; Return true for any inner rule.
+                    t)))))))
 
 (defun clojure-ts--match-function-call-arg (node parent _bol)
   "Match NODE if PARENT is a list expressing a function or macro call."
@@ -787,24 +904,6 @@ https://github.com/weavejester/cljfmt/blob/fb26b22f569724b05c93eb2502592dfc2de89
              (clojure-ts--keyword-node-p first-child)
              (clojure-ts--var-node-p first-child)))))
 
-(defun clojure-ts--match-expression-in-body (node parent _bol)
-  "Match NODE if it is an expression used in a body argument.
-PARENT is expected to be a list literal.
-See `treesit-simple-indent-rules'."
-  (and
-   (clojure-ts--list-node-p parent)
-   (let ((first-child (clojure-ts--node-child-skip-metadata parent 0)))
-     (and
-      (not
-       (clojure-ts--symbol-matches-p
-        ;; Symbols starting with this are false positives
-        (rx line-start (or "default" "deflate" "defer"))
-        first-child))
-      (not (clojure-ts--match-with-metadata node))
-      (clojure-ts--symbol-matches-p
-       clojure-ts--symbols-with-body-expressions-regexp
-       first-child)))))
-
 (defun clojure-ts--match-method-body (_node parent _bol)
   "Matches a `NODE' in the body of a `PARENT' method implementation.
 A method implementation referes to concrete implementations being defined in
@@ -885,7 +984,7 @@ forms like deftype, defrecord, reify, proxy, etc."
      (clojure-ts--match-docstring parent 0)
      ;; https://guide.clojure.style/#body-indentation
      (clojure-ts--match-method-body parent 2)
-     (clojure-ts--match-expression-in-body parent 2)
+     (clojure-ts--match-form-body parent 2)
      ;; https://guide.clojure.style/#threading-macros-alignment
      (clojure-ts--match-threading-macro-arg prev-sibling 0)
      ;; https://guide.clojure.style/#vertically-align-fn-args
diff --git a/test/clojure-ts-mode-indentation-test.el b/test/clojure-ts-mode-indentation-test.el
index d4d3517..23a432b 100644
--- a/test/clojure-ts-mode-indentation-test.el
+++ b/test/clojure-ts-mode-indentation-test.el
@@ -140,4 +140,103 @@ DESCRIPTION is a string with the description of the spec."
 (when-indenting-it "should support function calls via vars"
    "
 (#'foo 5
-       6)"))
+       6)")
+
+(when-indenting-it "should support block-0 expressions"
+  "
+(do (aligned)
+    (vertically))"
+
+  "
+(do
+  (indented)
+  (with-2-spaces))"
+
+  "
+(future
+  (body is indented))"
+
+  "
+(try
+  (something)
+  ;; A bit of block 2 rule
+  (catch Exception e
+    \"Third argument is indented with 2 spaces.\")
+  (catch ExceptionInfo
+         e-info
+    \"Second argument is aligned vertically with the first one.\"))")
+
+(when-indenting-it "should support block-1 expressions"
+  "
+(case x
+  2 (print 2)
+  3 (print 3)
+  (print \"Default\"))"
+
+  "
+(cond-> {}
+  :always (assoc :hello \"World\")
+  false (do nothing))"
+
+  "
+(with-precision 32
+  (/ (bigdec 20) (bigdec 30)))"
+
+  "
+(testing \"Something should work\"
+  (is (something-working?)))")
+
+(when-indenting-it "should support block-2 expressions"
+  "
+(are [x y]
+     (= x y)
+  2 3
+  4 5
+  6 6)"
+
+  "
+(as-> {} $
+  (assoc $ :hello \"World\"))"
+
+  "
+(as-> {}
+      my-map
+  (assoc my-map :hello \"World\"))"
+
+  "
+(defrecord MyThingR []
+  IProto
+  (foo [this x] x))")
+
+(when-indenting-it "should support inner-0 expressions"
+  "
+(fn named-lambda [x]
+  (+ x x))"
+
+  "
+(defmethod hello :world
+  [arg1 arg2]
+  (+ arg1 arg2))"
+
+  "
+(reify
+  AutoCloseable
+  (close
+    [this]
+    (is properly indented)))")
+
+(it "should prioritize custom semantic indentation rules"
+  (with-clojure-ts-buffer "
+(are [x y]
+     (= x y)
+  2 3
+  4 5
+  6 6)"
+    (setopt clojure-ts-semantic-indent-rules '(("are" . (:block 1))))
+    (indent-region (point-min) (point-max))
+    (expect (buffer-string) :to-equal "
+(are [x y]
+  (= x y)
+  2 3
+  4 5
+  6 6)"))))
diff --git a/test/samples/indentation.clj b/test/samples/indentation.clj
index f87870d..78a7aa6 100644
--- a/test/samples/indentation.clj
+++ b/test/samples/indentation.clj
@@ -79,8 +79,6 @@
   :another-keyword 2}
  "default value")
 
-
-
 (defprotocol IProto
   (foo [this x]
     "`this` is a docstring.")
@@ -121,7 +119,6 @@
           ([a b]
            b))})
 
-
 ^:foo
 (def a 1)
 
@@ -145,3 +142,71 @@
   "hello"
   [_foo]
   (+ 1 1))
+
+;;; Block 0 rule
+
+(do (aligned)
+    (vertically))
+
+(do
+  (indented)
+  (with-2-spaces))
+
+(future
+  (body is indented))
+
+(try
+  (something)
+  ;; A bit of block 2 rule
+  (catch Exception e
+    "Third argument is indented with 2 spaces.")
+  (catch ExceptionInfo
+         e-info
+    "Second argument is aligned vertically with the first one."))
+
+;;; Block 1 rule
+
+(case x
+  2 (print 2)
+  3 (print 3)
+  (print "Default"))
+
+(cond-> {}
+  :always (assoc :hello "World")
+  false (do nothing))
+
+(with-precision 32
+  (/ (bigdec 20) (bigdec 30)))
+
+(testing "Something should work"
+  (is (something-working?)))
+
+;;; Block 2 rule
+
+(are [x y]
+     (= x y)
+  2 3
+  4 5
+  6 6)
+
+(as-> {} $
+  (assoc $ :hello "World"))
+
+(as-> {}
+      my-map
+  (assoc my-map :hello "World"))
+
+;;; Inner 0 rule
+
+(fn named-lambda [x]
+  (+ x x))
+
+(defmethod hello :world
+  [arg1 arg2]
+  (+ arg1 arg2))
+
+(reify
+  AutoCloseable
+  (close
+    [this]
+    (is properly indented)))
diff --git a/test/test-helper.el b/test/test-helper.el
index c1cace3..f363644 100644
--- a/test/test-helper.el
+++ b/test/test-helper.el
@@ -24,7 +24,8 @@
 ;;; Code:
 
 (defmacro with-clojure-ts-buffer (text &rest body)
-  "Create a temporary buffer, insert TEXT,switch to clojure-ts-mode.
+  "Create a temporary buffer, insert TEXT, switch to `clojure-ts-mode'.
+
 And evaluate BODY."
   (declare (indent 1))
   `(with-temp-buffer