diff --git a/package.json b/package.json
index e08e218281682c206be4a4a8c7a0fe48ddebefe2..2d45e383be0539f940b1b4649f119f4f7a17ee22 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,10 @@
             "{}": false
           }
         }
-      ]
+      ],
+      "jsx-a11y/no-autofocus": [ 2, {
+        "ignoreNonDOM": true
+    }]
     }
   },
   "engines": {
diff --git a/web/package.json b/web/package.json
index 592925ffff30dca2c0caaea02821ae3192266029..2e0939f7db514507ead056e0212dc8e88711190d 100644
--- a/web/package.json
+++ b/web/package.json
@@ -18,13 +18,16 @@
     "@redwoodjs/forms": "6.5.1",
     "@redwoodjs/router": "6.5.1",
     "@redwoodjs/web": "6.5.1",
+    "@types/yup": "0.29.7",
     "axios": "^1.6.7",
+    "formik": "^2.4.5",
     "framer-motion": "^9",
     "prop-types": "15.8.1",
     "react": "18.2.0",
     "react-dom": "18.2.0",
     "react-query": "^3.39.3",
-    "ts-pattern": "^5.0.8"
+    "ts-pattern": "^5.0.8",
+    "yup": "0.29.3"
   },
   "devDependencies": {
     "@chakra-ui/storybook-addon": "^5.1.0",
diff --git a/web/src/design/theme.ts b/web/src/design/theme.ts
index cd118adfa0517551fa136a3ded57d4a46af38807..ede6bc7bc267e6124ee3b7fa369f28a0a0450b24 100644
--- a/web/src/design/theme.ts
+++ b/web/src/design/theme.ts
@@ -1,4 +1,4 @@
-import { ThemeConfig, extendTheme } from '@chakra-ui/react'
+import { ThemeConfig, extendTheme, Button } from '@chakra-ui/react'
 
 import { tableTheme } from './table-theme'
 
@@ -15,7 +15,10 @@ export const theme = extendTheme({
     currentColor: 'currentColor',
     current: 'currentColor',
   },
-  components: { Table: tableTheme },
+  components: {
+    Table: tableTheme,
+    Button,
+  },
   radii: {
     none: '0',
     xs: '2px',
diff --git a/web/src/pages/Patterns/add-pattern/add-pattern-button.tsx b/web/src/pages/Patterns/add-pattern/add-pattern-button.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8c8c6c5bf886bfcb2c7b5f3500b826c099329a9c
--- /dev/null
+++ b/web/src/pages/Patterns/add-pattern/add-pattern-button.tsx
@@ -0,0 +1,129 @@
+import React from 'react'
+
+import {
+  Button,
+  Modal,
+  ModalHeader,
+  ButtonProps,
+  useDisclosure,
+  ModalCloseButton,
+  ModalBody,
+  ModalOverlay,
+  ModalContent,
+  Text,
+  HStack,
+  VStack,
+  FormControl,
+  FormLabel,
+  Input,
+  ModalFooter,
+  FormErrorMessage,
+  Textarea,
+} from '@chakra-ui/react'
+import { useFormik } from 'formik'
+
+import { CreatePatternSchema, createPatternSchema } from './schema'
+
+const FORM_ID = 'add-pattern-form'
+
+export const AddPatternButton: React.FC<ButtonProps> = () => {
+  const { isOpen, onClose, onOpen } = useDisclosure()
+
+  const handleOnSubmit = (values: CreatePatternSchema) => {
+    console.log(values)
+  }
+
+  const {
+    handleSubmit,
+    handleReset,
+    handleChange,
+    setTouched,
+    values,
+    isSubmitting,
+    errors,
+    touched,
+    isValid,
+  } = useFormik({
+    enableReinitialize: true,
+    initialValues: {
+      title: '',
+      description: '',
+    },
+    validateOnBlur: true,
+    validateOnChange: true,
+    validateOnMount: true,
+    validationSchema: createPatternSchema,
+    onSubmit: handleOnSubmit,
+  })
+  return (
+    <>
+      <Button variant="solid" colorScheme="blue" onClick={onOpen}>
+        Crear
+      </Button>
+      <Modal isOpen={isOpen} onClose={onClose}>
+        <ModalOverlay />
+        <ModalContent>
+          <ModalHeader>
+            <Text fontSize="md">Crear un nuevo patron</Text>
+          </ModalHeader>
+          <ModalCloseButton />
+          <form id={FORM_ID} onSubmit={handleSubmit} onReset={handleReset}>
+            <ModalBody>
+              <VStack>
+                <FormControl isInvalid={Boolean(errors.title) && touched.title}>
+                  <FormLabel>
+                    <Text fontSize="sm">Titulo</Text>
+                  </FormLabel>
+                  <Input
+                    name="title"
+                    autoFocus
+                    value={values.title}
+                    onChange={(e) => {
+                      setTouched({ ...touched, title: true })
+                      handleChange(e)
+                    }}
+                  />
+                  <FormErrorMessage>{errors.title}</FormErrorMessage>
+                </FormControl>
+                <FormControl
+                  isInvalid={Boolean(errors.description) && touched.description}
+                >
+                  <FormLabel>
+                    <Text fontSize="sm">Descripcion</Text>
+                  </FormLabel>
+                  <Textarea
+                    name="description"
+                    value={values.description}
+                    onChange={(e) => {
+                      setTouched({ ...touched, description: true })
+                      handleChange(e)
+                    }}
+                  />
+                  <FormErrorMessage>{errors.description}</FormErrorMessage>
+                </FormControl>
+              </VStack>
+            </ModalBody>
+          </form>
+
+          <ModalFooter>
+            <HStack>
+              <Button variant="outline" onClick={onClose}>
+                Cancelar
+              </Button>
+              <Button
+                form={FORM_ID}
+                variant="solid"
+                colorScheme="blue"
+                type="submit"
+                isLoading={isSubmitting}
+                isDisabled={!isValid}
+              >
+                Guardar
+              </Button>
+            </HStack>
+          </ModalFooter>
+        </ModalContent>
+      </Modal>
+    </>
+  )
+}
diff --git a/web/src/pages/Patterns/add-pattern/index.ts b/web/src/pages/Patterns/add-pattern/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bd38f1b27c562d506290338853e4c59392d7d806
--- /dev/null
+++ b/web/src/pages/Patterns/add-pattern/index.ts
@@ -0,0 +1 @@
+export * from './add-pattern-button'
diff --git a/web/src/pages/Patterns/add-pattern/schema.ts b/web/src/pages/Patterns/add-pattern/schema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6c387448b9aaf2ffd7f038a9add02d57c063ccf2
--- /dev/null
+++ b/web/src/pages/Patterns/add-pattern/schema.ts
@@ -0,0 +1,18 @@
+import * as yup from 'yup'
+
+import { Pattern } from 'src/models'
+
+export type CreatePatternSchema = Pick<Pattern, 'title' | 'description'>
+
+const PATTERN_TITLE_MAX_LENGHT = 255
+
+export const createPatternSchema = yup.object().shape<CreatePatternSchema>({
+  title: yup
+    .string()
+    .max(
+      PATTERN_TITLE_MAX_LENGHT,
+      `El titulo debe tener menos de ${PATTERN_TITLE_MAX_LENGHT} caracteres`
+    )
+    .required('El titulo es requerido'),
+  description: yup.string().required('La descripcion es requerida'),
+})
diff --git a/web/src/pages/Patterns/patterns.tsx b/web/src/pages/Patterns/patterns.tsx
index 9897932f7cbd6b057b46851ba2e166e30d2e49b3..1e0226072be7f8892175812f305e953be9f71c01 100644
--- a/web/src/pages/Patterns/patterns.tsx
+++ b/web/src/pages/Patterns/patterns.tsx
@@ -24,6 +24,7 @@ import { Link, routes } from '@redwoodjs/router'
 
 import { Pattern } from 'src/models'
 
+import { AddPatternButton } from './add-pattern'
 import { GetPatternsQuery } from './query-builder'
 
 const tableHeaders = ['Titulo', 'Descripcion', '#Implementaciones', '']
@@ -79,11 +80,22 @@ export const Patterns: React.FC = () => {
                   </Tr>
                 ))
               )
-              .with({ status: 'loading' }, () => <Spinner />)
+              .with({ status: 'loading' }, () => (
+                <Tr>
+                  <Td colSpan={tableHeaders.length}>
+                    <HStack justifyContent="center">
+                      <Spinner />
+                    </HStack>
+                  </Td>
+                </Tr>
+              ))
               .otherwise(() => null)}
           </Tbody>
         </Table>
       </TableContainer>
+      <HStack w="full" justifyContent="flex-end">
+        <AddPatternButton />
+      </HStack>
     </VStack>
   )
 }
@@ -94,7 +106,6 @@ type ActionsRowProps = {
 const ActionsRow: React.FC<ActionsRowProps> = ({ pattern }) => {
   return (
     <HStack spacing="0">
-      {/* Linter yells but is fine https://github.com/redwoodjs/redwood/issues/1742 */}
       <Link to={routes.pattern({ id: pattern.id })}>
         <IconButton
           aria-label="view pattern"
diff --git a/yarn.lock b/yarn.lock
index 6998661d37e4f86df34c901783d4b73138b86f11..574d2efebac27694c33064f54c9fe0e25c28e6f6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1683,7 +1683,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/runtime@npm:^7.23.8, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.2, @babel/runtime@npm:^7.7.2":
+"@babel/runtime@npm:^7.10.5, @babel/runtime@npm:^7.23.8, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.2, @babel/runtime@npm:^7.7.2":
   version: 7.24.0
   resolution: "@babel/runtime@npm:7.24.0"
   dependencies:
@@ -8189,6 +8189,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/hoist-non-react-statics@npm:^3.3.1":
+  version: 3.3.5
+  resolution: "@types/hoist-non-react-statics@npm:3.3.5"
+  dependencies:
+    "@types/react": "*"
+    hoist-non-react-statics: ^3.3.0
+  checksum: 2a3b64bf3d9817d7830afa60ee314493c475fb09570a64e7737084cd482d2177ebdddf888ce837350bac51741278b077683facc9541f052d4bbe8487b4e3e618
+  languageName: node
+  linkType: hard
+
 "@types/html-minifier-terser@npm:^6.0.0":
   version: 6.1.0
   resolution: "@types/html-minifier-terser@npm:6.1.0"
@@ -8670,6 +8680,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/yup@npm:0.29.7":
+  version: 0.29.7
+  resolution: "@types/yup@npm:0.29.7"
+  checksum: d04cbfbbd6907f83f8f9cf974fd1e019cbd68d1a2125ca67f7bea12623ea47cc015bcc01cc3952f0fba9dec46a5091026976181dd6081cfd6b85908b0303590b
+  languageName: node
+  linkType: hard
+
 "@typescript-eslint/eslint-plugin@npm:5.62.0":
   version: 5.62.0
   resolution: "@typescript-eslint/eslint-plugin@npm:5.62.0"
@@ -12568,6 +12585,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"deepmerge@npm:^2.1.1":
+  version: 2.2.1
+  resolution: "deepmerge@npm:2.2.1"
+  checksum: 4379288cabd817587cee92a095ea65d18317b45e48010a2e0d87982b5f432239a144f9c8ebd4ab090cc21f0cb47e51ebfe32921f329b3b3084a2711d5d63e450
+  languageName: node
+  linkType: hard
+
 "default-browser-id@npm:3.0.0":
   version: 3.0.0
   resolution: "default-browser-id@npm:3.0.0"
@@ -14669,6 +14693,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"fn-name@npm:~3.0.0":
+  version: 3.0.0
+  resolution: "fn-name@npm:3.0.0"
+  checksum: ec02fe860c8afad4c77ed4412a2915513415ae84d266b06cead06da4aad4149f2297f4ada385359ec8778247cf5c7576577d014bbd994c4326222a6ead6dffa9
+  languageName: node
+  linkType: hard
+
 "focus-lock@npm:^1.0.0":
   version: 1.0.0
   resolution: "focus-lock@npm:1.0.0"
@@ -14758,6 +14789,24 @@ __metadata:
   languageName: node
   linkType: hard
 
+"formik@npm:^2.4.5":
+  version: 2.4.5
+  resolution: "formik@npm:2.4.5"
+  dependencies:
+    "@types/hoist-non-react-statics": ^3.3.1
+    deepmerge: ^2.1.1
+    hoist-non-react-statics: ^3.3.0
+    lodash: ^4.17.21
+    lodash-es: ^4.17.21
+    react-fast-compare: ^2.0.1
+    tiny-warning: ^1.0.2
+    tslib: ^2.0.0
+  peerDependencies:
+    react: ">=16.8.0"
+  checksum: 61f0d9eb092edd122f0d2988ca3d0a01073bde38af977e96ba9818382dc1fefd4cdb016cd61f08443055a748bbbbe2a95347d4528b81cc5c1c6f75865fc84927
+  languageName: node
+  linkType: hard
+
 "forwarded@npm:0.2.0":
   version: 0.2.0
   resolution: "forwarded@npm:0.2.0"
@@ -15615,7 +15664,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2":
+"hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2":
   version: 3.3.2
   resolution: "hoist-non-react-statics@npm:3.3.2"
   dependencies:
@@ -17958,6 +18007,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"lodash-es@npm:^4.17.11, lodash-es@npm:^4.17.21":
+  version: 4.17.21
+  resolution: "lodash-es@npm:4.17.21"
+  checksum: fb407355f7e6cd523a9383e76e6b455321f0f153a6c9625e21a8827d10c54c2a2341bd2ae8d034358b60e07325e1330c14c224ff582d04612a46a4f0479ff2f2
+  languageName: node
+  linkType: hard
+
 "lodash.chunk@npm:^4.2.0":
   version: 4.2.0
   resolution: "lodash.chunk@npm:4.2.0"
@@ -20655,6 +20711,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"property-expr@npm:^2.0.2":
+  version: 2.0.6
+  resolution: "property-expr@npm:2.0.6"
+  checksum: 69b7da15038a1146d6447c69c445306f66a33c425271235bb20507f1846dbf9577a8f9dfafe8acbfcb66f924b270157f155248308f026a68758f35fc72265b3c
+  languageName: node
+  linkType: hard
+
 "proxy-addr@npm:^2.0.7, proxy-addr@npm:~2.0.7":
   version: 2.0.7
   resolution: "proxy-addr@npm:2.0.7"
@@ -21009,6 +21072,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"react-fast-compare@npm:^2.0.1":
+  version: 2.0.4
+  resolution: "react-fast-compare@npm:2.0.4"
+  checksum: f0300c677e95198b5f993cbb8a983dab09586157dc678f9e2b5b29ff941b6677a8776fbbdc425ce102fad86937e36bb45cfcfd797f006270b97ccf287ebfb885
+  languageName: node
+  linkType: hard
+
 "react-focus-lock@npm:^2.9.4":
   version: 2.9.6
   resolution: "react-focus-lock@npm:2.9.6"
@@ -23164,7 +23234,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"synchronous-promise@npm:^2.0.15":
+"synchronous-promise@npm:^2.0.13, synchronous-promise@npm:^2.0.15":
   version: 2.0.17
   resolution: "synchronous-promise@npm:2.0.17"
   checksum: 1babe643d8417789ef6e5a2f3d4b8abcda2de236acd09bbe2c98f6be82c0a2c92ed21a6e4f934845fa8de18b1435a9cba1e8c3d945032e8a532f076224c024b1
@@ -23435,6 +23505,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"tiny-warning@npm:^1.0.2":
+  version: 1.0.3
+  resolution: "tiny-warning@npm:1.0.3"
+  checksum: ef8531f581b30342f29670cb41ca248001c6fd7975ce22122bd59b8d62b4fc84ad4207ee7faa95cde982fa3357cd8f4be650142abc22805538c3b1392d7084fa
+  languageName: node
+  linkType: hard
+
 "title-case@npm:3.0.3, title-case@npm:^3.0.3":
   version: 3.0.3
   resolution: "title-case@npm:3.0.3"
@@ -23549,6 +23626,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"toposort@npm:^2.0.2":
+  version: 2.0.2
+  resolution: "toposort@npm:2.0.2"
+  checksum: ab9ca91fce4b972ccae9e2f539d755bf799a0c7eb60da07fd985fce0f14c159ed1e92305ff55697693b5bc13e300f5417db90e2593b127d421c9f6c440950222
+  languageName: node
+  linkType: hard
+
 "totalist@npm:^3.0.0":
   version: 3.0.1
   resolution: "totalist@npm:3.0.1"
@@ -24652,13 +24736,16 @@ __metadata:
     "@redwoodjs/web": 6.5.1
     "@types/react": 18.2.37
     "@types/react-dom": 18.2.15
+    "@types/yup": 0.29.7
     axios: ^1.6.7
+    formik: ^2.4.5
     framer-motion: ^9
     prop-types: 15.8.1
     react: 18.2.0
     react-dom: 18.2.0
     react-query: ^3.39.3
     ts-pattern: ^5.0.8
+    yup: 0.29.3
   languageName: unknown
   linkType: soft
 
@@ -25443,6 +25530,21 @@ __metadata:
   languageName: node
   linkType: hard
 
+"yup@npm:0.29.3":
+  version: 0.29.3
+  resolution: "yup@npm:0.29.3"
+  dependencies:
+    "@babel/runtime": ^7.10.5
+    fn-name: ~3.0.0
+    lodash: ^4.17.15
+    lodash-es: ^4.17.11
+    property-expr: ^2.0.2
+    synchronous-promise: ^2.0.13
+    toposort: ^2.0.2
+  checksum: f49e621dcbdf8a8010df35d920025f27884d24702750cd124cf7839ab40fc6f7e94b00e6ff888dbdbc176b77e3acf89c6dcc11561052af9afee0b293c05fd210
+  languageName: node
+  linkType: hard
+
 "zen-observable-ts@npm:^1.2.5":
   version: 1.2.5
   resolution: "zen-observable-ts@npm:1.2.5"