diff --git a/examples/editor/package.json b/examples/editor/package.json
index 1101a3d4ee..791f589532 100644
--- a/examples/editor/package.json
+++ b/examples/editor/package.json
@@ -12,7 +12,8 @@
     "@blocknote/core": "^0.8.1",
     "@blocknote/react": "^0.8.1",
     "react": "^18.2.0",
-    "react-dom": "^18.2.0"
+    "react-dom": "^18.2.0",
+    "zod": "^3.21.4"
   },
   "devDependencies": {
     "@types/react": "^18.0.25",
diff --git a/examples/editor/src/App.tsx b/examples/editor/src/App.tsx
index 55ce63eb93..c4c11f162b 100644
--- a/examples/editor/src/App.tsx
+++ b/examples/editor/src/App.tsx
@@ -1,8 +1,67 @@
 // import logo from './logo.svg'
+import { z } from 'zod';
+import { DefaultBlockSchema, defaultBlockSchema } from '@blocknote/core';
+import { BlockNoteView, useBlockNote, createReactBlockSpec, ReactSlashMenuItem, defaultReactSlashMenuItems } from "@blocknote/react";
 import "@blocknote/core/style.css";
-import { BlockNoteView, useBlockNote } from "@blocknote/react";
 import styles from "./App.module.css";
 
+export const AccordionBlock = createReactBlockSpec({
+  type: 'accordion',
+  propSchema: z.object({
+    label: z.string(),
+    autoLayout: z.object({
+      enabled: z.boolean(),
+    }).optional(),
+  }),
+  render: ({ editor, block }) => {
+    return (
+      <>
+        <h2 className='mb-2'>{block.props.label}</h2>
+        {
+          block.props.autoLayout?.enabled ? 
+            (
+              <div className='flex flex-col'>
+                Enabled
+              </div>
+            ) : 
+          <></>
+        }
+      </>
+    );
+  },
+  containsInlineContent: false,
+});
+
+// Creates a slash menu item for inserting an image block.
+export const insertAccordion = new ReactSlashMenuItem<
+  DefaultBlockSchema & { accordion: typeof AccordionBlock }
+>(
+  'Insert Accordion',
+  (editor) => {
+    editor.insertBlocks(
+      [
+        // Default values are set here
+        {
+          type: 'accordion',
+          props: {
+            label: 'Default',
+            autoLayout: {
+              enabled: true
+            }
+          },
+        },
+      ],
+      editor.getTextCursorPosition().block,
+      'before'
+    );
+  },
+  ['accordion'],
+  'Containers',
+  <>+</>,
+  'Used to group content in an accordion.'
+);
+
+
 type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any };
 
 function App() {
@@ -10,6 +69,14 @@ function App() {
     onEditorContentChange: (editor) => {
       console.log(editor.topLevelBlocks);
     },
+    blockSchema: {
+      ...defaultBlockSchema,
+      accordion: AccordionBlock,
+    },
+    slashCommands: [
+      ...defaultReactSlashMenuItems,
+      insertAccordion
+    ],
     editorDOMAttributes: {
       class: styles.editor,
       "data-test": "editor",
diff --git a/package-lock.json b/package-lock.json
index f66c089540..0e7e071cd2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -25,7 +25,8 @@
         "@blocknote/core": "^0.8.1",
         "@blocknote/react": "^0.8.1",
         "react": "^18.2.0",
-        "react-dom": "^18.2.0"
+        "react-dom": "^18.2.0",
+        "zod": "^3.21.4"
       },
       "devDependencies": {
         "@types/react": "^18.0.25",
@@ -20459,6 +20460,14 @@
       "resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz",
       "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA=="
     },
+    "node_modules/zod": {
+      "version": "3.21.4",
+      "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
+      "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==",
+      "funding": {
+        "url": "https://github.com/sponsors/colinhacks"
+      }
+    },
     "node_modules/zwitch": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
@@ -20506,7 +20515,8 @@
         "uuid": "^8.3.2",
         "y-prosemirror": "1.0.20",
         "y-protocols": "^1.0.5",
-        "yjs": "^13.6.1"
+        "yjs": "^13.6.1",
+        "zod": "^3.21.4"
       },
       "devDependencies": {
         "@types/hast": "^2.3.4",
diff --git a/packages/core/package.json b/packages/core/package.json
index 91c2b7fedc..beb372216d 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -82,7 +82,8 @@
     "uuid": "^8.3.2",
     "y-prosemirror": "1.0.20",
     "y-protocols": "^1.0.5",
-    "yjs": "^13.6.1"
+    "yjs": "^13.6.1",
+    "zod": "^3.21.4"
   },
   "devDependencies": {
     "@types/hast": "^2.3.4",
diff --git a/packages/core/src/api/formatConversions/formatConversions.test.ts b/packages/core/src/api/formatConversions/formatConversions.test.ts
index 0d8e4054c1..5da57ecec8 100644
--- a/packages/core/src/api/formatConversions/formatConversions.test.ts
+++ b/packages/core/src/api/formatConversions/formatConversions.test.ts
@@ -35,8 +35,8 @@ beforeEach(() => {
       id: UniqueID.options.generateID(),
       type: "heading",
       props: {
-        backgroundColor: "default",
-        textColor: "default",
+        backgroundColor: "transparent",
+        textColor: "black",
         textAlignment: "left",
         level: "1",
       },
@@ -53,8 +53,8 @@ beforeEach(() => {
       id: UniqueID.options.generateID(),
       type: "paragraph",
       props: {
-        backgroundColor: "default",
-        textColor: "default",
+        backgroundColor: "transparent",
+        textColor: "black",
         textAlignment: "left",
       },
       content: [
@@ -70,8 +70,8 @@ beforeEach(() => {
       id: UniqueID.options.generateID(),
       type: "bulletListItem",
       props: {
-        backgroundColor: "default",
-        textColor: "default",
+        backgroundColor: "transparent",
+        textColor: "black",
         textAlignment: "left",
       },
       content: [
@@ -87,8 +87,8 @@ beforeEach(() => {
       id: UniqueID.options.generateID(),
       type: "numberedListItem",
       props: {
-        backgroundColor: "default",
-        textColor: "default",
+        backgroundColor: "transparent",
+        textColor: "black",
         textAlignment: "left",
       },
       content: [
@@ -116,8 +116,8 @@ Paragraph
       id: UniqueID.options.generateID(),
       type: "heading",
       props: {
-        backgroundColor: "default",
-        textColor: "default",
+        backgroundColor: "transparent",
+        textColor: "black",
         textAlignment: "left",
         level: "1",
       },
@@ -133,8 +133,8 @@ Paragraph
           id: UniqueID.options.generateID(),
           type: "paragraph",
           props: {
-            backgroundColor: "default",
-            textColor: "default",
+            backgroundColor: "transparent",
+            textColor: "black",
             textAlignment: "left",
           },
           content: [
@@ -149,8 +149,8 @@ Paragraph
               id: UniqueID.options.generateID(),
               type: "bulletListItem",
               props: {
-                backgroundColor: "default",
-                textColor: "default",
+                backgroundColor: "transparent",
+                textColor: "black",
                 textAlignment: "left",
               },
               content: [
@@ -165,8 +165,8 @@ Paragraph
                   id: UniqueID.options.generateID(),
                   type: "numberedListItem",
                   props: {
-                    backgroundColor: "default",
-                    textColor: "default",
+                    backgroundColor: "transparent",
+                    textColor: "black",
                     textAlignment: "left",
                   },
                   content: [
@@ -200,8 +200,8 @@ Paragraph
       id: UniqueID.options.generateID(),
       type: "paragraph",
       props: {
-        backgroundColor: "default",
-        textColor: "default",
+        backgroundColor: "transparent",
+        textColor: "black",
         textAlignment: "left",
       },
       content: [
@@ -323,8 +323,8 @@ Paragraph
       id: UniqueID.options.generateID(),
       type: "paragraph",
       props: {
-        backgroundColor: "default",
-        textColor: "default",
+        backgroundColor: "transparent",
+        textColor: "black",
         textAlignment: "left",
       },
       content: [
@@ -343,8 +343,8 @@ Paragraph
       id: UniqueID.options.generateID(),
       type: "paragraph",
       props: {
-        backgroundColor: "default",
-        textColor: "default",
+        backgroundColor: "transparent",
+        textColor: "black",
         textAlignment: "left",
       },
       content: [
@@ -379,8 +379,8 @@ Paragraph
       id: UniqueID.options.generateID(),
       type: "paragraph",
       props: {
-        backgroundColor: "default",
-        textColor: "default",
+        backgroundColor: "transparent",
+        textColor: "black",
         textAlignment: "left",
       },
       content: [
@@ -415,8 +415,8 @@ Paragraph
       id: UniqueID.options.generateID(),
       type: "bulletListItem",
       props: {
-        backgroundColor: "default",
-        textColor: "default",
+        backgroundColor: "transparent",
+        textColor: "black",
         textAlignment: "left",
       },
       content: [
@@ -432,8 +432,8 @@ Paragraph
       id: UniqueID.options.generateID(),
       type: "bulletListItem",
       props: {
-        backgroundColor: "default",
-        textColor: "default",
+        backgroundColor: "transparent",
+        textColor: "black",
         textAlignment: "left",
       },
       content: [
@@ -448,8 +448,8 @@ Paragraph
           id: UniqueID.options.generateID(),
           type: "bulletListItem",
           props: {
-            backgroundColor: "default",
-            textColor: "default",
+            backgroundColor: "transparent",
+            textColor: "black",
             textAlignment: "left",
           },
           content: [
@@ -464,8 +464,8 @@ Paragraph
               id: UniqueID.options.generateID(),
               type: "bulletListItem",
               props: {
-                backgroundColor: "default",
-                textColor: "default",
+                backgroundColor: "transparent",
+                textColor: "black",
                 textAlignment: "left",
               },
               content: [
@@ -481,8 +481,8 @@ Paragraph
               id: UniqueID.options.generateID(),
               type: "paragraph",
               props: {
-                backgroundColor: "default",
-                textColor: "default",
+                backgroundColor: "transparent",
+                textColor: "black",
                 textAlignment: "left",
               },
               content: [
@@ -498,8 +498,8 @@ Paragraph
               id: UniqueID.options.generateID(),
               type: "numberedListItem",
               props: {
-                backgroundColor: "default",
-                textColor: "default",
+                backgroundColor: "transparent",
+                textColor: "black",
                 textAlignment: "left",
               },
               content: [
@@ -515,8 +515,8 @@ Paragraph
               id: UniqueID.options.generateID(),
               type: "numberedListItem",
               props: {
-                backgroundColor: "default",
-                textColor: "default",
+                backgroundColor: "transparent",
+                textColor: "black",
                 textAlignment: "left",
               },
               content: [
@@ -532,8 +532,8 @@ Paragraph
               id: UniqueID.options.generateID(),
               type: "numberedListItem",
               props: {
-                backgroundColor: "default",
-                textColor: "default",
+                backgroundColor: "transparent",
+                textColor: "black",
                 textAlignment: "left",
               },
               content: [
@@ -548,8 +548,8 @@ Paragraph
                   id: UniqueID.options.generateID(),
                   type: "numberedListItem",
                   props: {
-                    backgroundColor: "default",
-                    textColor: "default",
+                    backgroundColor: "transparent",
+                    textColor: "black",
                     textAlignment: "left",
                   },
                   content: [
@@ -567,8 +567,8 @@ Paragraph
               id: UniqueID.options.generateID(),
               type: "bulletListItem",
               props: {
-                backgroundColor: "default",
-                textColor: "default",
+                backgroundColor: "transparent",
+                textColor: "black",
                 textAlignment: "left",
               },
               content: [
@@ -586,8 +586,8 @@ Paragraph
           id: UniqueID.options.generateID(),
           type: "bulletListItem",
           props: {
-            backgroundColor: "default",
-            textColor: "default",
+            backgroundColor: "transparent",
+            textColor: "black",
             textAlignment: "left",
           },
           content: [
@@ -605,8 +605,8 @@ Paragraph
       id: UniqueID.options.generateID(),
       type: "bulletListItem",
       props: {
-        backgroundColor: "default",
-        textColor: "default",
+        backgroundColor: "transparent",
+        textColor: "black",
         textAlignment: "left",
       },
       content: [
diff --git a/packages/core/src/api/nodeConversions/nodeConversions.ts b/packages/core/src/api/nodeConversions/nodeConversions.ts
index dc7f53c226..3b07844d4f 100644
--- a/packages/core/src/api/nodeConversions/nodeConversions.ts
+++ b/packages/core/src/api/nodeConversions/nodeConversions.ts
@@ -4,6 +4,7 @@ import {
   Block,
   BlockSchema,
   PartialBlock,
+  Props,
 } from "../../extensions/Blocks/api/blockTypes";
 
 import { defaultProps } from "../../extensions/Blocks/api/defaultBlocks";
@@ -148,6 +149,10 @@ export function blockToNode<BSchema extends BlockSchema>(
 
   let contentNode: Node;
 
+  if (!block.props) {
+    throw new Error("Block props are undefined");    
+  }
+
   if (!block.content) {
     contentNode = schema.nodes[type].create(block.props);
   } else if (typeof block.content === "string") {
@@ -378,22 +383,24 @@ export function nodeToBlock<BSchema extends BlockSchema>(
     id = UniqueID.options.generateID();
   }
 
-  const props: any = {};
+  const blockSpec = blockSchema[blockInfo.contentType.name];
+  
+  if (!blockSpec) {
+    throw Error(
+      "Block is of an unrecognized type: " + blockInfo.contentType.name
+    );
+  }
+
+  const props: Props<typeof blockSpec.propSchema> = Object.create(null);
+  
   for (const [attr, value] of Object.entries({
     ...blockInfo.node.attrs,
     ...blockInfo.contentNode.attrs,
   })) {
-    const blockSpec = blockSchema[blockInfo.contentType.name];
-    if (!blockSpec) {
-      throw Error(
-        "Block is of an unrecognized type: " + blockInfo.contentType.name
-      );
-    }
-
-    const propSchema = blockSpec.propSchema;
-
-    if (attr in propSchema) {
-      props[attr] = value;
+    if (attr in blockSpec.propSchema.shape) {
+      if (props && typeof props === "object") {
+        props[attr as keyof typeof props] = value as never;
+      }
     }
     // Block ids are stored as node attributes the same way props are, so we
     // need to ensure we don't attempt to read block ids as props.
diff --git a/packages/core/src/extensions/Blocks/api/block.ts b/packages/core/src/extensions/Blocks/api/block.ts
index 77604299a9..b27f2dfc7d 100644
--- a/packages/core/src/extensions/Blocks/api/block.ts
+++ b/packages/core/src/extensions/Blocks/api/block.ts
@@ -29,20 +29,25 @@ export function propsToAttributes<
 ) {
   const tiptapAttributes: Record<string, Attribute> = {};
 
-  Object.entries(blockConfig.propSchema).forEach(([name, spec]) => {
+  Object.entries(blockConfig.propSchema.shape).forEach(([name, spec]) => {
     tiptapAttributes[name] = {
-      default: spec.default,
+      default: null, // It will be provided by `insertBlocks`
       keepOnSplit: true,
       // Props are displayed in kebab-case as HTML attributes. If a prop's
       // value is the same as its default, we don't display an HTML
       // attribute for it.
       parseHTML: (element) => element.getAttribute(camelToDataKebab(name)),
-      renderHTML: (attributes) =>
-        attributes[name] !== spec.default
-          ? {
-              [camelToDataKebab(name)]: attributes[name],
-            }
-          : {},
+      renderHTML: (attributes) => {
+        const parsed = spec.safeParse(attributes[name]);
+
+        if (!parsed.success || attributes[camelToDataKebab(name)] !== parsed.data) {
+          return {
+            [camelToDataKebab(name)]: typeof attributes[name] === 'string' ? attributes[name] : null,
+          }
+        }
+
+        return {}
+      },
     };
   });
 
@@ -162,7 +167,7 @@ export function createBlockSpec<
 
         // Gets BlockNote editor instance
         const editor = this.options.editor! as BlockNoteEditor<
-          BSchema & { [k in BType]: BlockSpec<BType, PSchema> }
+          { [k in BType]: BlockSpec<BType, PSchema> }
         >;
         // Gets position of the node
         if (typeof getPos === "boolean") {
diff --git a/packages/core/src/extensions/Blocks/api/blockTypes.ts b/packages/core/src/extensions/Blocks/api/blockTypes.ts
index ee49d9921d..88a65ee1fe 100644
--- a/packages/core/src/extensions/Blocks/api/blockTypes.ts
+++ b/packages/core/src/extensions/Blocks/api/blockTypes.ts
@@ -1,4 +1,5 @@
 /** Define the main block types **/
+import { z } from "zod";
 import { Node, NodeConfig } from "@tiptap/core";
 import { BlockNoteEditor } from "../../../BlockNoteEditor";
 import { InlineContent, PartialInlineContent } from "./inlineContentTypes";
@@ -31,28 +32,17 @@ export type TipTapNode<
   group: "blockContent";
 };
 
-// Defines a single prop spec, which includes the default value the prop should
-// take and possible values it can take.
-export type PropSpec = {
-  values?: readonly string[];
-  default: string;
-};
-
 // Defines multiple block prop specs. The key of each prop is the name of the
 // prop, while the value is a corresponding prop spec. This should be included
 // in a block config or schema. From a prop schema, we can derive both the props'
 // internal implementation (as TipTap node attributes) and the type information
 // for the external API.
-export type PropSchema = Record<string, PropSpec>;
+export type PropSchema = z.SomeZodObject;
 
 // Defines Props objects for use in Block objects in the external API. Converts
-// each prop spec into a union type of its possible values, or a string if no
-// values are specified.
-export type Props<PSchema extends PropSchema> = {
-  [PType in keyof PSchema]: PSchema[PType]["values"] extends readonly string[]
-    ? PSchema[PType]["values"][number]
-    : string;
-};
+// each prop spec into a union type of its possible values, or the type of the
+//  'default' property if values are not specified.
+export type Props<PSchema extends PropSchema> = z.infer<PSchema>;
 
 // Defines the config for a single block. Meant to be used as an argument to
 // `createBlockSpec`, which will create a new block spec from it. This is the
@@ -84,7 +74,7 @@ export type BlockConfig<
      * This is typed generically. If you want an editor with your custom schema, you need to
      * cast it manually, e.g.: `const e = editor as BlockNoteEditor<typeof mySchema>;`
      */
-    editor: BlockNoteEditor<BSchema & { [k in Type]: BlockSpec<Type, PSchema> }>
+    editor: BlockNoteEditor<{ [k in Type]: BlockSpec<Type, PSchema> }>
     // (note) if we want to fix the manual cast, we need to prevent circular references and separate block definition and render implementations
     // or allow manually passing <BSchema>, but that's not possible without passing the other generics because Typescript doesn't support partial inferred generics
   ) => ContainsInlineContent extends true
@@ -161,7 +151,7 @@ type PartialBlocksWithoutChildren<BSchema extends BlockSchema> = {
   [BType in keyof BSchema]: Partial<{
     id: string;
     type: BType;
-    props: Partial<Props<BSchema[BType]["propSchema"]>>;
+    props: Props<BSchema[BType]["propSchema"]>;
     content: PartialInlineContent[] | string;
   }>;
 };
diff --git a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts
index d60d716cce..7ab1790d58 100644
--- a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts
+++ b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts
@@ -1,44 +1,50 @@
+import { z } from "zod";
+
 import { HeadingBlockContent } from "../nodes/BlockContent/HeadingBlockContent/HeadingBlockContent";
 import { BulletListItemBlockContent } from "../nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent";
 import { NumberedListItemBlockContent } from "../nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent";
 import { ParagraphBlockContent } from "../nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent";
-import { PropSchema, TypesMatch } from "./blockTypes";
+
+export const defaultPropSchema = z.object({
+  backgroundColor: z.enum(["transparent", "red", "orange", "yellow", "blue"]).optional(),
+  textColor: z.enum(["black", "red", "orange", "yellow"]).optional(),
+  textAlignment: z.enum(["left", "center", "right", "justify"]).optional(),
+});
 
 export const defaultProps = {
-  backgroundColor: {
-    default: "transparent" as const,
-  },
-  textColor: {
-    default: "black" as const, // TODO
-  },
-  textAlignment: {
-    default: "left" as const,
-    values: ["left", "center", "right", "justify"] as const,
-  },
-} satisfies PropSchema;
+  backgroundColor: "transparent",
+  textColor: "black",
+  textAlignment: "left",
+} satisfies z.infer<typeof defaultPropSchema>;
 
 export type DefaultProps = typeof defaultProps;
 
 export const defaultBlockSchema = {
   paragraph: {
-    propSchema: defaultProps,
+    propSchema: defaultPropSchema,
+    props: defaultProps,
     node: ParagraphBlockContent,
   },
   heading: {
-    propSchema: {
+    propSchema: defaultPropSchema.merge(z.object({
+      level: z.enum(["1", "2", "3"]).optional(),
+    })),
+    props: {
       ...defaultProps,
-      level: { default: "1", values: ["1", "2", "3"] as const },
+      level: "1",
     },
     node: HeadingBlockContent,
   },
   bulletListItem: {
-    propSchema: defaultProps,
+    propSchema: defaultPropSchema,
+    props: defaultProps,
     node: BulletListItemBlockContent,
   },
   numberedListItem: {
-    propSchema: defaultProps,
+    propSchema: defaultPropSchema,
+    props: defaultProps,
     node: NumberedListItemBlockContent,
   },
 } as const;
 
-export type DefaultBlockSchema = TypesMatch<typeof defaultBlockSchema>;
+export type DefaultBlockSchema = typeof defaultBlockSchema;
\ No newline at end of file
diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts b/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts
index 3120ba8253..d3fad13b76 100644
--- a/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts
+++ b/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts
@@ -200,7 +200,7 @@ export const BlockContainer = Node.create<IBlock>({
                 : state.schema.nodes[block.type],
               {
                 ...contentNode.attrs,
-                ...block.props,
+                ...block.props!,
               }
             );
 
@@ -208,7 +208,7 @@ export const BlockContainer = Node.create<IBlock>({
             // attributes.
             state.tr.setNodeMarkup(startPos - 1, undefined, {
               ...node.attrs,
-              ...block.props,
+              ...block.props!,
             });
           }
 
diff --git a/packages/react/src/BlockSideMenu/components/DefaultButtons/BlockColorsButton.tsx b/packages/react/src/BlockSideMenu/components/DefaultButtons/BlockColorsButton.tsx
index 3fc435b275..4515fb4e8b 100644
--- a/packages/react/src/BlockSideMenu/components/DefaultButtons/BlockColorsButton.tsx
+++ b/packages/react/src/BlockSideMenu/components/DefaultButtons/BlockColorsButton.tsx
@@ -29,11 +29,15 @@ export const BlockColorsButton = <BSchema extends BlockSchema>(
     setOpened(true);
   }, []);
 
+  if (!(props.block.props && typeof props.block.props === 'object')) {
+    return <></>;
+  }
+
   if (
     !("textColor" in props.block.props) ||
     !("backgroundColor" in props.block.props)
   ) {
-    return null;
+    return <></>;
   }
 
   return (
@@ -56,8 +60,8 @@ export const BlockColorsButton = <BSchema extends BlockSchema>(
           style={{ marginLeft: "5px" }}>
           <ColorPicker
             iconSize={18}
-            textColor={props.block.props.textColor || "default"}
-            backgroundColor={props.block.props.backgroundColor || "default"}
+            textColor={props.block.props.textColor as string || "default"}
+            backgroundColor={props.block.props.backgroundColor as string || "default"}
             setTextColor={(color) =>
               props.editor.updateBlock(props.block, {
                 props: { textColor: color },
diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx
index dde4fe6342..5f4cb1cd85 100644
--- a/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx
+++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx
@@ -1,8 +1,9 @@
 import {
   BlockNoteEditor,
   BlockSchema,
-  DefaultProps,
   PartialBlock,
+  defaultPropSchema,
+  Props,
 } from "@blocknote/core";
 import { useCallback, useMemo } from "react";
 import { IconType } from "react-icons";
@@ -14,7 +15,7 @@ import {
 } from "react-icons/ri";
 import { ToolbarButton } from "../../../SharedComponents/Toolbar/components/ToolbarButton";
 
-type TextAlignment = DefaultProps["textAlignment"]["values"][number];
+type TextAlignment = Exclude<Props<typeof defaultPropSchema>["textAlignment"], undefined>;
 
 const icons: Record<TextAlignment, IconType> = {
   left: RiAlignLeft,
@@ -32,14 +33,14 @@ export const TextAlignButton = <BSchema extends BlockSchema>(props: {
 
     if (selection) {
       for (const block of selection.blocks) {
-        if (!("textAlignment" in block.props)) {
+        if (block.props && typeof block.props === 'object' && !("textAlignment" in block.props)) {
           return false;
         }
       }
     } else {
       const block = props.editor.getTextCursorPosition().block;
 
-      if (!("textAlignment" in block.props)) {
+      if (block.props && typeof block.props === 'object' && !("textAlignment" in block.props)) {
         return false;
       }
     }
@@ -78,7 +79,7 @@ export const TextAlignButton = <BSchema extends BlockSchema>(props: {
     <ToolbarButton
       onClick={() => setTextAlignment(props.textAlignment)}
       isSelected={
-        props.editor.getTextCursorPosition().block.props.textAlignment ===
+        (props.editor.getTextCursorPosition().block.props as any).textAlignment ===
         props.textAlignment
       }
       mainTooltip={
diff --git a/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx b/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx
index b6ce8fe6f5..bf0799896d 100644
--- a/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx
+++ b/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx
@@ -2,6 +2,7 @@ import {
   BlockNoteEditor,
   BlockSchema,
   DefaultBlockSchema,
+  defaultBlockSchema,
 } from "@blocknote/core";
 import { useEffect, useState } from "react";
 import { IconType } from "react-icons";
@@ -25,7 +26,7 @@ const headingIcons: Record<HeadingLevels, IconType> = {
 
 const shouldShow = (schema: BlockSchema) => {
   const paragraph = "paragraph" in schema;
-  const heading = "heading" in schema && "level" in schema.heading.propSchema;
+  const heading = "heading" in schema;
   const bulletListItem = "bulletListItem" in schema;
   const numberedListItem = "numberedListItem" in schema;
 
@@ -52,18 +53,24 @@ export const BlockTypeDropdown = <BSchema extends BlockSchema>(props: {
   // the default block schema is being used
   let editor = props.editor as any as BlockNoteEditor<DefaultBlockSchema>;
 
-  const headingItems = editor.schema.heading.propSchema.level.values.map(
+  const parsedDefaultProps = defaultBlockSchema.heading.propSchema.safeParse(defaultBlockSchema.heading.props)
+
+  if (!parsedDefaultProps.success) {
+    throw new Error("Default heading values are not valid");
+  }
+
+  const headingItems = (['1', '2', '3'] as const).map(
     (level) => ({
       onClick: () => {
         editor.focus();
         editor.updateBlock(block, {
           type: "heading",
-          props: { level: level },
+          props: { ...parsedDefaultProps.data, level: level },
         });
       },
       text: "Heading " + level,
       icon: headingIcons[level],
-      isSelected: block.type === "heading" && block.props.level === level,
+      isSelected: block.type === "heading" && (block.props as any).level === level,
     })
   );
 
diff --git a/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx b/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx
index 85b3650d0b..49772061a5 100644
--- a/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx
+++ b/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx
@@ -22,9 +22,9 @@ export const FormattingToolbar = <BSchema extends BlockSchema>(props: {
       <ToggledStyleButton editor={props.editor} toggledStyle={"underline"} />
       <ToggledStyleButton editor={props.editor} toggledStyle={"strike"} />
 
-      <TextAlignButton editor={props.editor as any} textAlignment={"left"} />
-      <TextAlignButton editor={props.editor as any} textAlignment={"center"} />
-      <TextAlignButton editor={props.editor as any} textAlignment={"right"} />
+      <TextAlignButton editor={props.editor} textAlignment={"left"} />
+      <TextAlignButton editor={props.editor} textAlignment={"center"} />
+      <TextAlignButton editor={props.editor} textAlignment={"right"} />
 
       <ColorStyleButton editor={props.editor} />
 
diff --git a/packages/react/src/ReactBlockSpec.tsx b/packages/react/src/ReactBlockSpec.tsx
index 7b8aa7884b..63fca6e704 100644
--- a/packages/react/src/ReactBlockSpec.tsx
+++ b/packages/react/src/ReactBlockSpec.tsx
@@ -88,8 +88,9 @@ export function createReactBlockSpec<
 
         // Add props as HTML attributes in kebab-case with "data-" prefix
         const htmlAttributes: Record<string, string> = {};
+
         for (const [attribute, value] of Object.entries(props.node.attrs)) {
-          if (attribute in blockConfig.propSchema) {
+          if (attribute in blockConfig.propSchema.shape) {
             htmlAttributes[camelToDataKebab(attribute)] = value;
           }
         }
diff --git a/tests/utils/customblocks/Alert.tsx b/tests/utils/customblocks/Alert.tsx
index 8df5df14f1..81baf48c3f 100644
--- a/tests/utils/customblocks/Alert.tsx
+++ b/tests/utils/customblocks/Alert.tsx
@@ -1,4 +1,6 @@
-import { createBlockSpec, defaultProps } from "@blocknote/core";
+import { z } from "zod";
+import React from "react";
+import { createBlockSpec, defaultProps, defaultPropSchema } from "@blocknote/core";
 import { ReactSlashMenuItem } from "@blocknote/react";
 import { RiAlertFill } from "react-icons/ri";
 
@@ -23,13 +25,15 @@ const values = {
 
 export const Alert = createBlockSpec({
   type: "alert" as const,
-  propSchema: {
+  propSchema: z.object({
+    textAlignment: defaultPropSchema.shape.textAlignment,
+    textColor: defaultPropSchema.shape.textColor,
+    type: z.enum(["warning", "error", "info", "success"]),
+  }),
+  props: {
     textAlignment: defaultProps.textAlignment,
     textColor: defaultProps.textColor,
-    type: {
-      default: "warning",
-      values: ["warning", "error", "info", "success"],
-    },
+    type: "warning",
   } as const,
   containsInlineContent: true,
   render: (block, editor) => {
diff --git a/tests/utils/customblocks/Button.tsx b/tests/utils/customblocks/Button.tsx
index 5e861e63ae..a5afe611f4 100644
--- a/tests/utils/customblocks/Button.tsx
+++ b/tests/utils/customblocks/Button.tsx
@@ -1,10 +1,15 @@
-import { createBlockSpec, defaultProps } from "@blocknote/core";
+import { z } from "zod";
+import React from "react";
+import { createBlockSpec, defaultProps, defaultPropSchema } from "@blocknote/core";
 import { ReactSlashMenuItem } from "@blocknote/react";
 import { RiRadioButtonFill } from "react-icons/ri";
 
 export const Button = createBlockSpec({
   type: "button" as const,
-  propSchema: {
+  propSchema: z.object({
+    backgroundColor: defaultPropSchema.shape.backgroundColor,
+  }),
+  props: {
     backgroundColor: defaultProps.backgroundColor,
   } as const,
   containsInlineContent: false,
@@ -15,7 +20,7 @@ export const Button = createBlockSpec({
       editor.insertBlocks(
         [
           {
-            type: "paragraph",
+            type: "button",
             content: "Hello World",
           },
         ],
diff --git a/tests/utils/customblocks/Embed.tsx b/tests/utils/customblocks/Embed.tsx
index 9fbd822012..5437873455 100644
--- a/tests/utils/customblocks/Embed.tsx
+++ b/tests/utils/customblocks/Embed.tsx
@@ -1,13 +1,16 @@
+import { z } from "zod";
+import React from "react";
 import { createBlockSpec } from "@blocknote/core";
 import { ReactSlashMenuItem } from "@blocknote/react";
 import { RiLayout5Fill } from "react-icons/ri";
 
 export const Embed = createBlockSpec({
   type: "embed" as const,
-  propSchema: {
-    src: {
-      default: "https://www.youtube.com/embed/wjfuB8Xjhc4",
-    },
+  propSchema: z.object({
+    src: z.string().url(),
+  }),
+  props: {
+    src: "https://www.youtube.com/embed/wjfuB8Xjhc4"
   } as const,
   containsInlineContent: false,
   render: (block) => {
diff --git a/tests/utils/customblocks/Image.tsx b/tests/utils/customblocks/Image.tsx
index 9b03dc0770..d7300d4410 100644
--- a/tests/utils/customblocks/Image.tsx
+++ b/tests/utils/customblocks/Image.tsx
@@ -1,14 +1,17 @@
-import { createBlockSpec, defaultProps } from "@blocknote/core";
+import { z } from "zod";
+import React from "react";
+import { createBlockSpec, defaultPropSchema, defaultProps } from "@blocknote/core";
 import { ReactSlashMenuItem } from "@blocknote/react";
 import { RiImage2Fill } from "react-icons/ri";
 
 export const Image = createBlockSpec({
   type: "image" as const,
-  propSchema: {
+  propSchema: defaultPropSchema.merge(z.object({
+    src: z.string().url(),
+  })),
+  props: {
     ...defaultProps,
-    src: {
-      default: "https://via.placeholder.com/1000",
-    },
+    src: "https://via.placeholder.com/1000"
   } as const,
   containsInlineContent: true,
   render: (block) => {
diff --git a/tests/utils/customblocks/ReactAlert.tsx b/tests/utils/customblocks/ReactAlert.tsx
index e89b8586c4..fdc9078a25 100644
--- a/tests/utils/customblocks/ReactAlert.tsx
+++ b/tests/utils/customblocks/ReactAlert.tsx
@@ -1,4 +1,6 @@
-import { defaultProps } from "@blocknote/core";
+import { z } from "zod";
+import React from "react";
+import { defaultProps, defaultPropSchema } from "@blocknote/core";
 import {
   createReactBlockSpec,
   InlineContent,
@@ -28,13 +30,13 @@ const values = {
 
 export const ReactAlert = createReactBlockSpec({
   type: "reactAlert" as const,
-  propSchema: {
+  propSchema: defaultPropSchema.merge(z.object({
+    type: z.enum(["warning", "error", "info", "success"]),
+  })),
+  props: {
     textAlignment: defaultProps.textAlignment,
     textColor: defaultProps.textColor,
-    type: {
-      default: "warning",
-      values: ["warning", "error", "info", "success"],
-    },
+    type: "warning",
   } as const,
   containsInlineContent: true,
   render: (props) => {
diff --git a/tests/utils/customblocks/ReactImage.tsx b/tests/utils/customblocks/ReactImage.tsx
index f410fbd13a..315a569b41 100644
--- a/tests/utils/customblocks/ReactImage.tsx
+++ b/tests/utils/customblocks/ReactImage.tsx
@@ -1,18 +1,21 @@
+import { z } from "zod";
+import React from "react";
 import {
   InlineContent,
   createReactBlockSpec,
   ReactSlashMenuItem,
 } from "@blocknote/react";
-import { defaultProps } from "@blocknote/core";
+import { defaultProps, defaultPropSchema } from "@blocknote/core";
 import { RiImage2Fill } from "react-icons/ri";
 
 export const ReactImage = createReactBlockSpec({
   type: "reactImage" as const,
-  propSchema: {
+  propSchema: defaultPropSchema.merge(z.object({
+    src: z.string().url(),
+  })),
+  props: {
     ...defaultProps,
-    src: {
-      default: "https://via.placeholder.com/1000",
-    },
+    src: "https://via.placeholder.com/1000"
   } as const,
   containsInlineContent: true,
   render: ({ block }) => {
diff --git a/tests/utils/customblocks/Separator.tsx b/tests/utils/customblocks/Separator.tsx
index 39e84067d7..1b37afc8de 100644
--- a/tests/utils/customblocks/Separator.tsx
+++ b/tests/utils/customblocks/Separator.tsx
@@ -1,10 +1,13 @@
+import { z } from "zod";
+import React from "react";
 import { createBlockSpec } from "@blocknote/core";
 import { ReactSlashMenuItem } from "@blocknote/react";
 import { RiSeparator } from "react-icons/ri";
 
 export const Separator = createBlockSpec({
   type: "separator" as const,
-  propSchema: {} as const,
+  propSchema: z.object({}),
+  props: {} as const,
   containsInlineContent: false,
   render: () => {
     const separator = document.createElement("div");
diff --git a/tests/utils/customblocks/TableOfContents.tsx b/tests/utils/customblocks/TableOfContents.tsx
index e29844d952..f98a97c857 100644
--- a/tests/utils/customblocks/TableOfContents.tsx
+++ b/tests/utils/customblocks/TableOfContents.tsx
@@ -1,3 +1,5 @@
+import { z } from "zod";
+import React from "react";
 import {
   Block,
   BlockSchema,
@@ -43,7 +45,8 @@ function createHeadingElements(block: Block<BlockSchema>) {
 
 export const TableOfContents = createBlockSpec({
   type: "toc" as const,
-  propSchema: {} as const,
+  propSchema: z.object({}),
+  props: {} as const,
   containsInlineContent: false,
   render: (_, editor) => {
     const toc = document.createElement("ol");
@@ -51,7 +54,7 @@ export const TableOfContents = createBlockSpec({
     editor.onEditorContentChange(() => {
       toc.innerHTML = "";
       for (const block of editor.topLevelBlocks) {
-        if (block.type === "heading") {
+        if ((block.type as any) === "heading") {
           toc.appendChild(createHeadingElements(block));
         }
       }
diff --git a/tests/utils/draghandle.ts b/tests/utils/draghandle.ts
index a576070b85..b3c4714d98 100644
--- a/tests/utils/draghandle.ts
+++ b/tests/utils/draghandle.ts
@@ -23,5 +23,5 @@ export async function getDragHandleYCoord(page: Page, selector: string) {
   await moveMouseOverElement(page, element);
   await page.waitForSelector(DRAG_HANDLE_SELECTOR);
   const boundingBox = await page.locator(DRAG_HANDLE_SELECTOR).boundingBox();
-  return boundingBox.y;
+  return boundingBox?.y;
 }
diff --git a/tests/utils/mouse.ts b/tests/utils/mouse.ts
index 308b4f39b7..56d1ad9398 100644
--- a/tests/utils/mouse.ts
+++ b/tests/utils/mouse.ts
@@ -3,29 +3,29 @@ import { DRAG_HANDLE_SELECTOR } from "./const";
 
 async function getElementLeftCoords(page: Page, element: Locator) {
   const boundingBox = await element.boundingBox();
-  const centerY = boundingBox.y + boundingBox.height / 2;
+  const centerY = boundingBox!.y + boundingBox!.height / 2;
 
-  return { x: boundingBox.x + 1, y: centerY };
+  return { x: boundingBox!.x + 1, y: centerY };
 }
 
 async function getElementRightCoords(page: Page, element: Locator) {
   const boundingBox = await element.boundingBox();
-  const centerY = boundingBox.y + boundingBox.height / 2;
+  const centerY = boundingBox!.y + boundingBox!.height / 2;
 
-  return { x: boundingBox.x + boundingBox.width - 1, y: centerY };
+  return { x: boundingBox!.x + boundingBox!.width - 1, y: centerY };
 }
 
 async function getElementCenterCoords(page: Page, element: Locator) {
   const boundingBox = await element.boundingBox();
-  const centerX = boundingBox.x + boundingBox.width / 2;
-  const centerY = boundingBox.y + boundingBox.height / 2;
+  const centerX = boundingBox!.x + boundingBox!.width / 2;
+  const centerY = boundingBox!.y + boundingBox!.height / 2;
 
   return { x: centerX, y: centerY };
 }
 
 export async function moveMouseOverElement(page: Page, element: Locator) {
   const boundingBox = await element.boundingBox();
-  const coords = { x: boundingBox.x, y: boundingBox.y };
+  const coords = { x: boundingBox!.x, y: boundingBox!.y };
   await page.mouse.move(coords.x, coords.y, { steps: 5 });
 }