├── .prettierignore
├── src
├── Label
│ ├── index.ts
│ └── Label.tsx
├── Submit
│ ├── index.ts
│ └── Submit.tsx
├── vite-env.d.ts
├── Input
│ ├── index.ts
│ ├── Text.tsx
│ └── Input.tsx
├── Form
│ ├── index.ts
│ ├── FormContext.ts
│ └── Form.tsx
├── Textarea
│ ├── index.ts
│ ├── Container.tsx
│ ├── Text.tsx
│ └── Textarea.tsx
├── index.ts
├── utils.ts
├── types.ts
└── troika-three-text.d.ts
├── .storybook
├── assets
│ └── fonts
│ │ ├── fonts.d.ts
│ │ ├── Poppins-Bold.ttf
│ │ ├── Montserrat-Bold.ttf
│ │ ├── MajorMonoDisplay.ttf
│ │ └── PlayfairDisplay-Regular.ttf
├── manager-head.html
├── style.css
├── main.ts
├── preview.tsx
├── stories
│ ├── Input.stories.tsx
│ ├── Textarea.stories.tsx
│ └── Form.stories.tsx
└── common.tsx
├── .prettierrc
├── tsconfig.node.json
├── .gitignore
├── .github
├── workflows
│ ├── build-ci.yml
│ ├── semantic-release.yml
│ └── build-storybook.yml
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── dependabot.yml
└── pull_request_template.md
├── tsconfig.json
├── .eslintrc.cjs
├── vite.config.ts
├── package.json
└── README.md
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
--------------------------------------------------------------------------------
/src/Label/index.ts:
--------------------------------------------------------------------------------
1 | export { Label } from "./Label";
2 |
--------------------------------------------------------------------------------
/src/Submit/index.ts:
--------------------------------------------------------------------------------
1 | export { Submit } from "./Submit";
2 |
--------------------------------------------------------------------------------
/.storybook/assets/fonts/fonts.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.ttf";
2 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/Input/index.ts:
--------------------------------------------------------------------------------
1 | export { Input } from "./Input";
2 | export { InputText } from "./Text";
3 |
--------------------------------------------------------------------------------
/src/Form/index.ts:
--------------------------------------------------------------------------------
1 | export { Form } from "./Form";
2 | export { useFormContext } from "./FormContext";
3 |
--------------------------------------------------------------------------------
/src/Textarea/index.ts:
--------------------------------------------------------------------------------
1 | export { Textarea } from "./Textarea";
2 | export { TextareaText } from "./Text";
3 |
--------------------------------------------------------------------------------
/.storybook/manager-head.html:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/.storybook/style.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0 !important;
4 | }
5 |
6 | body {
7 | height: 100dvh;
8 | }
9 |
--------------------------------------------------------------------------------
/.storybook/assets/fonts/Poppins-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JMBeresford/r3f-form/HEAD/.storybook/assets/fonts/Poppins-Bold.ttf
--------------------------------------------------------------------------------
/.storybook/assets/fonts/Montserrat-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JMBeresford/r3f-form/HEAD/.storybook/assets/fonts/Montserrat-Bold.ttf
--------------------------------------------------------------------------------
/.storybook/assets/fonts/MajorMonoDisplay.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JMBeresford/r3f-form/HEAD/.storybook/assets/fonts/MajorMonoDisplay.ttf
--------------------------------------------------------------------------------
/.storybook/assets/fonts/PlayfairDisplay-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JMBeresford/r3f-form/HEAD/.storybook/assets/fonts/PlayfairDisplay-Regular.ttf
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Input";
2 | export * from "./Label";
3 | export * from "./Textarea";
4 | export * from "./Form";
5 | export * from "./Submit";
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "trailingComma": "es5",
4 | "singleQuote": false,
5 | "tabWidth": 2,
6 | "printWidth": 100,
7 | "useTabs": false
8 | }
9 |
--------------------------------------------------------------------------------
/src/Form/FormContext.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | export const FormContext = React.createContext>({ current: null });
4 |
5 | export function useFormContext() {
6 | return React.useContext(FormContext);
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | export function lerp(x: number, y: number, t: number): number {
2 | return x + (y - x) * t;
3 | }
4 |
5 | export function damp(x: number, y: number, lambda: number, dt: number): number {
6 | return lerp(x, y, 1 - Math.exp(-lambda * dt));
7 | }
8 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true
9 | },
10 | "include": ["vite.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | # storybook artifacts
27 | storybook-static
28 |
--------------------------------------------------------------------------------
/.storybook/main.ts:
--------------------------------------------------------------------------------
1 | import type { StorybookConfig } from "@storybook/react-vite";
2 |
3 | const config: StorybookConfig = {
4 | stories: [
5 | "../src/**/*.mdx",
6 | "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)",
7 | "./stories/**/*.stories.@(js|jsx|mjs|ts|tsx)",
8 | ],
9 | framework: {
10 | name: "@storybook/react-vite",
11 | options: {},
12 | },
13 | staticDirs: ["./assets"],
14 | docs: {
15 | autodocs: "tag",
16 | },
17 | };
18 |
19 | export default config;
20 |
--------------------------------------------------------------------------------
/.github/workflows/build-ci.yml:
--------------------------------------------------------------------------------
1 | name: Build CI
2 |
3 | on:
4 | pull_request:
5 | types: ["opened", "reopened", "synchronize"]
6 | branches: ["main"]
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | - uses: actions/setup-node@v3
14 | with:
15 | node-version: "18.x"
16 | - name: Setup
17 | run: npm ci
18 | - name: Format
19 | run: npm run format:check
20 | - name: Lint
21 | run: npm run lint
22 | - name: Build
23 | run: npm run build
24 |
--------------------------------------------------------------------------------
/.storybook/preview.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import "./style.css";
3 | import type { Preview } from "@storybook/react";
4 | import { Scene } from "./common";
5 |
6 | const preview: Preview = {
7 | decorators: [
8 | (Story) => (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | ),
17 | ],
18 | parameters: {
19 | actions: { argTypesRegex: "^on[A-Z].*" },
20 | },
21 | };
22 |
23 | export default preview;
24 |
--------------------------------------------------------------------------------
/.github/workflows/semantic-release.yml:
--------------------------------------------------------------------------------
1 | name: Semantic Release
2 |
3 | on:
4 | push:
5 | branches: ["main"]
6 |
7 | workflow_dispatch:
8 | # allows manual trigger in github web UI
9 |
10 | jobs:
11 | release:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 | - uses: actions/setup-node@v3
16 | with:
17 | node-version: "20.11.1"
18 | - run: npm ci
19 | - run: npm run build
20 | - name: Release
21 | env:
22 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
23 | GITHUB_TOKEN: ${{ secrets.REPO_TOKEN }}
24 | run: npm run semantic-release
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "feat: "
5 | labels: enhancement
6 | assignees: ""
7 | ---
8 |
9 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 | "moduleResolution": "bundler",
9 | "allowImportingTsExtensions": true,
10 | "resolveJsonModule": true,
11 | "isolatedModules": true,
12 | "noEmit": true,
13 | "jsx": "react-jsx",
14 | "strict": true,
15 | "noUnusedLocals": true,
16 | "noUnusedParameters": true,
17 | "noFallthroughCasesInSwitch": true,
18 | "noImplicitAny": true
19 | },
20 | "include": ["src", ".storybook"],
21 | "references": [
22 | {
23 | "path": "./tsconfig.node.json"
24 | }
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { Color } from "@react-three/fiber";
2 | import { TextRenderInfo } from "troika-three-text";
3 |
4 | export type TroikaTextProps = {
5 | color?: Color;
6 | fontSize?: number;
7 | letterSpacing?: number;
8 | font?: string;
9 | depthOffset?: number;
10 | outlineWidth?: number | string;
11 | outlineOffsetX?: number | string;
12 | outlineOffsetY?: number | string;
13 | outlineBlur?: number | string;
14 | outlineColor?: Color;
15 | outlineOpacity?: number;
16 | strokeWidth?: number | string;
17 | strokeColor?: Color;
18 | strokeOpacity?: number;
19 | fillOpacity?: number;
20 | fillColor?: Color;
21 | onSync?: (troika: { textRenderInfo: TextRenderInfo }) => void;
22 | } & Omit;
23 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | "eslint:recommended",
6 | "plugin:@typescript-eslint/recommended",
7 | "plugin:react-hooks/recommended",
8 | "plugin:react/recommended",
9 | "plugin:storybook/recommended",
10 | ],
11 | ignorePatterns: ["dist", ".eslintrc.cjs"],
12 | parser: "@typescript-eslint/parser",
13 | plugins: ["react-refresh", "prettier"],
14 | rules: {
15 | "react/no-unknown-property": "off",
16 | "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
17 | "react/jsx-uses-react": "off",
18 | "react/react-in-jsx-scope": "off",
19 | },
20 | settings: {
21 | react: {
22 | version: "detect",
23 | },
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "npm" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "monthly"
12 | groups:
13 | major-updates:
14 | update-types:
15 | - "major"
16 | ignore:
17 | - dependency-name: "*"
18 | update-types: ["version-update:semver-minor", "version-update:semver-patch"]
19 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import dts from "vite-plugin-dts";
3 | import react from "@vitejs/plugin-react";
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | build: {
8 | lib: {
9 | entry: "src/index.ts",
10 | name: "r3f-form",
11 | fileName: "index",
12 | formats: ["es"],
13 | },
14 | rollupOptions: {
15 | external: [
16 | "react",
17 | "@react-three/fiber",
18 | "@react-three/drei",
19 | "troika-three-text",
20 | "react-dom",
21 | "three",
22 | "react/jsx-runtime",
23 | ],
24 | output: {
25 | globals: {
26 | react: "React",
27 | },
28 | },
29 | },
30 | },
31 | plugins: [dts({ exclude: "src/utils.ts" }), react()],
32 | });
33 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | - **Please check if the PR fulfills these requirements**
2 |
3 | * [ ] The commit message follows our guidelines: https://www.conventionalcommits.org/
4 | * [ ] Docs and READMEs have been added / updated (if required)
5 | * [ ] Storybook(s) have been added / updated (if required)
6 |
7 | - **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...)
8 | e.g. fixes #, resolves # with additional descriptions if needed
9 |
10 | - **What is the current behavior?** (You can also link to an open issue here)
11 |
12 | - **What is the new behavior (if this is a feature change)?**
13 |
14 | - **Does this PR introduce a breaking change?** (What changes might users need to make in their application due to this PR?)
15 |
16 | - **Other information**:
17 |
--------------------------------------------------------------------------------
/src/Textarea/Container.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Color } from "@react-three/fiber";
3 | import { useCursor } from "@react-three/drei";
4 |
5 | type Props = {
6 | width: number;
7 | height: number;
8 | backgroundColor: Color;
9 | backgroundOpacity: number;
10 | } & JSX.IntrinsicElements["mesh"];
11 |
12 | const Container = (props: Props) => {
13 | const { width, height, backgroundColor, backgroundOpacity, ...restProps } = props;
14 |
15 | const [hovered, setHovered] = React.useState(false);
16 | useCursor(hovered, "text");
17 |
18 | return (
19 | setHovered(true)}
21 | onPointerLeave={() => setHovered(false)}
22 | renderOrder={1}
23 | {...restProps}
24 | >
25 |
26 |
32 |
33 | );
34 | };
35 |
36 | export default Container;
37 |
--------------------------------------------------------------------------------
/.github/workflows/build-storybook.yml:
--------------------------------------------------------------------------------
1 | # Simple workflow for deploying static content to GitHub Pages
2 | name: Deploy Storybook to Pages
3 |
4 | on:
5 | # Runs on pushes targeting the default branch
6 | push:
7 | branches: ["main"]
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
13 | permissions:
14 | contents: read
15 | pages: write
16 | id-token: write
17 |
18 | # Allow one concurrent deployment
19 | concurrency:
20 | group: "pages"
21 | cancel-in-progress: true
22 |
23 | jobs:
24 | buildAndDeploy:
25 | environment:
26 | name: github-pages
27 | url: ${{ steps.deployment.outputs.page_url }}
28 | runs-on: ubuntu-latest
29 | steps:
30 | - uses: actions/checkout@v3
31 | - uses: actions/setup-node@v3
32 | with:
33 | node-version: "16.x"
34 | registry-url: https://registry.npmjs.org/
35 | - run: npm ci
36 | - run: npm run build-storybook
37 | - name: Setup Pages
38 | uses: actions/configure-pages@v2
39 | - name: Upload artifact
40 | uses: actions/upload-pages-artifact@v1
41 | with:
42 | # Upload only storybook static files
43 | path: "storybook-static"
44 | - name: Deploy to GitHub Pages
45 | id: deployment
46 | uses: actions/deploy-pages@v1
47 |
--------------------------------------------------------------------------------
/src/Label/Label.tsx:
--------------------------------------------------------------------------------
1 | import { Color } from "@react-three/fiber";
2 | import { Text } from "@react-three/drei";
3 |
4 | type Props = {
5 | characters?: string;
6 | color?: Color;
7 | fontSize?: number;
8 | maxWidth?: number;
9 | lineHeight?: number;
10 | letterSpacing?: number;
11 | textAlign?: "left" | "right" | "center" | "justify";
12 | font?: string;
13 | anchorX?: number | "left" | "center" | "right";
14 | anchorY?: number | "top" | "top-baseline" | "middle" | "bottom-baseline" | "bottom";
15 | depthOffset?: number;
16 | overflowWrap?: "normal" | "break-word";
17 | whiteSpace?: "normal" | "overflowWrap" | "nowrap";
18 | outlineWidth?: number | string;
19 | outlineOffsetX?: number | string;
20 | outlineOffsetY?: number | string;
21 | outlineBlur?: number | string;
22 | outlineOpacity?: number;
23 | strokeWidth?: number | string;
24 | strokeOpacity?: number;
25 | fillOpacity?: number;
26 | debugSDF?: boolean;
27 | text?: string;
28 | } & JSX.IntrinsicElements["mesh"];
29 |
30 | const Label = (props: Props) => {
31 | const {
32 | color = "black",
33 | text,
34 | anchorX = "center",
35 | anchorY = "bottom",
36 | fontSize = 0.07,
37 | ...restProps
38 | } = props;
39 |
40 | return (
41 | <>
42 |
43 |
44 | {text}
45 |
46 |
47 |
48 | >
49 | );
50 | };
51 |
52 | export { Label };
53 |
--------------------------------------------------------------------------------
/src/Form/Form.tsx:
--------------------------------------------------------------------------------
1 | import { useThree } from "@react-three/fiber";
2 | import * as React from "react";
3 | import * as ReactDOM from "react-dom/client";
4 | import { FormContext } from "./FormContext";
5 |
6 | type Props = Omit, "ref">;
7 |
8 | const Form = React.forwardRef((props: Props, ref: React.ForwardedRef) => {
9 | const { children, ...restProps } = props;
10 | const events = useThree((s) => s.events);
11 | const gl = useThree((s) => s.gl);
12 | const root = React.useRef(null);
13 | const domRef = React.useRef(null!);
14 | const target = (events.connected || gl.domElement.parentNode) as HTMLElement;
15 | React.useImperativeHandle(ref, () => domRef.current);
16 |
17 | const [domEl] = React.useState(() => document.createElement("div"));
18 |
19 | React.useLayoutEffect(() => {
20 | root.current = ReactDOM.createRoot(domEl);
21 | const curRoot = root.current;
22 |
23 | target?.appendChild(domEl);
24 |
25 | return () => {
26 | target?.removeChild(domEl);
27 | curRoot.unmount();
28 | };
29 | }, [target, domEl]);
30 |
31 | React.useLayoutEffect(() => {
32 | root.current?.render(
33 |
39 | );
40 | });
41 |
42 | return {children};
43 | });
44 |
45 | Form.displayName = "Form";
46 |
47 | export { Form };
48 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "bug: "
5 | labels: bug
6 | assignees: ""
7 | ---
8 |
9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **To Reproduce**
13 | Steps to reproduce the behavior:
14 |
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Code Sandbox**
24 | Sometimes it is easier to show rather than tell. Try to reproduce the bug in a codesandbox and give us the link. You can use this template to start:
25 |
26 | https://codesandbox.io/p/sandbox/r3f-form-bug-report-5lwyrr
27 |
28 | **Screenshots**
29 | If applicable, add screenshots to help explain your problem.
30 |
31 | **Desktop (please complete the following information):**
32 |
33 | - OS: [e.g. macOS, windows]
34 | - OS Version [e.g. 13.3]
35 | - Browser [e.g. chrome, safari]
36 | - Browser Version [e.g. 22.5]
37 |
38 | **Smartphone (please complete the following information):**
39 |
40 | - Device: [e.g. iPhone6]
41 | - OS and version: [e.g. iOS8.1]
42 | - Browser [e.g. stock browser, safari]
43 | - Browser Version [e.g. 22]
44 |
45 | **Package Versions**
46 |
47 | - Node.js version [e.g. 16.x]
48 | - NPM version [e.g. 8.9.x]
49 | - React Three Fiber version [e.g. 9.x]
50 | - THREE.js version [e.g. 0.146.x]
51 | - Drei version [e.g. 8.x]
52 | - Troika-Three-Text version [e.g. 1.x]
53 |
54 | **Additional context**
55 | Add any other context about the problem here.
56 |
--------------------------------------------------------------------------------
/src/Textarea/Text.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Text as TextImpl, useMask } from "@react-three/drei";
3 | import { Color } from "@react-three/fiber";
4 |
5 | type Props = {
6 | characters?: string;
7 | color?: Color;
8 | fontSize?: number;
9 | maxWidth?: number;
10 | lineHeight?: number;
11 | letterSpacing?: number;
12 | textAlign?: "left" | "right" | "center" | "justify";
13 | font?: string;
14 | anchorX?: number | "left" | "center" | "right";
15 | anchorY?: number | "top" | "top-baseline" | "middle" | "bottom-baseline" | "bottom";
16 | depthOffset?: number;
17 | outlineWidth?: number | string;
18 | outlineOffsetX?: number | string;
19 | outlineOffsetY?: number | string;
20 | outlineBlur?: number | string;
21 | outlineOpacity?: number;
22 | strokeWidth?: number | string;
23 | strokeOpacity?: number;
24 | fillOpacity?: number;
25 | debugSDF?: boolean;
26 | onSync?: (troika: unknown) => void;
27 | text?: string;
28 | } & JSX.IntrinsicElements["mesh"];
29 |
30 | const TextareaText = React.forwardRef((props: Props, ref) => {
31 | const {
32 | fontSize = 0.07,
33 | anchorX = "left",
34 | anchorY = "middle",
35 | depthOffset = 0.5,
36 | color = "black",
37 | text,
38 | ...restProps
39 | } = props;
40 |
41 | const stencil = useMask(2);
42 |
43 | return (
44 |
52 | {text}
53 |
54 |
55 | );
56 | });
57 |
58 | TextareaText.displayName = "TextareaText";
59 |
60 | export { TextareaText };
61 |
--------------------------------------------------------------------------------
/.storybook/stories/Input.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Input, Label, InputText } from "../../src";
3 | import type { Meta, StoryObj } from "@storybook/react";
4 |
5 | const meta: Meta = {
6 | component: Default,
7 | title: "Input",
8 | };
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | function Default() {
14 | return ;
15 | }
16 |
17 | function WithLabel() {
18 | return (
19 |
20 |
21 |
22 |
23 | );
24 | }
25 |
26 | function WithPlaceholder() {
27 | return ;
28 | }
29 |
30 | function WithColor() {
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |
38 | );
39 | }
40 |
41 | function WithCustomFont() {
42 | return (
43 |
44 |
45 |
46 |
47 |
48 |
49 | );
50 | }
51 |
52 | export const DefaultStory: Story = {
53 | name: "Default",
54 | render: () => ,
55 | };
56 |
57 | export const WithLabelStory: Story = {
58 | name: "With Label",
59 | render: () => ,
60 | };
61 |
62 | export const WithPlaceholderStory: Story = {
63 | name: "With Placeholder",
64 | render: () => ,
65 | };
66 |
67 | export const WithColorStory: Story = {
68 | name: "With Color",
69 | render: () => ,
70 | };
71 |
72 | export const WithCustomFontStory: Story = {
73 | name: "With Custom Font",
74 | render: () => ,
75 | };
76 |
--------------------------------------------------------------------------------
/.storybook/stories/Textarea.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Textarea, Label, TextareaText } from "../../src";
3 | import type { Meta, StoryObj } from "@storybook/react";
4 |
5 | const meta: Meta = {
6 | component: Default,
7 | title: "Textarea",
8 | };
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | function Default() {
14 | return ;
15 | }
16 |
17 | function WithLabel() {
18 | return (
19 |
20 |
21 |
22 |
23 | );
24 | }
25 |
26 | function WithPlaceholder() {
27 | return ;
28 | }
29 |
30 | function WithColor() {
31 | return (
32 |
33 |
34 |
37 |
38 | );
39 | }
40 |
41 | function WithCustomFont() {
42 | return (
43 |
44 |
45 |
48 |
49 | );
50 | }
51 |
52 | export const DefaultStory: Story = {
53 | name: "Default",
54 | render: () => ,
55 | };
56 |
57 | export const WithLabelStory: Story = {
58 | name: "With Label",
59 | render: () => ,
60 | };
61 |
62 | export const WithPlaceholderStory: Story = {
63 | name: "With Placeholder",
64 | render: () => ,
65 | };
66 |
67 | export const WithColorStory: Story = {
68 | name: "With Color",
69 | render: () => ,
70 | };
71 |
72 | export const WithCustomFontStory: Story = {
73 | name: "With Custom Font",
74 | render: () => ,
75 | };
76 |
--------------------------------------------------------------------------------
/src/Input/Text.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Text as TextImpl, useMask } from "@react-three/drei";
3 | import { Color } from "@react-three/fiber";
4 | import { TextRenderInfo } from "troika-three-text";
5 |
6 | type Props = {
7 | characters?: string;
8 | color?: Color;
9 | fontSize?: number;
10 | maxWidth?: number;
11 | lineHeight?: number;
12 | letterSpacing?: number;
13 | textAlign?: "left" | "right" | "center" | "justify";
14 | font?: string;
15 | anchorX?: number | "left" | "center" | "right";
16 | anchorY?: number | "top" | "top-baseline" | "middle" | "bottom-baseline" | "bottom";
17 | depthOffset?: number;
18 | overflowWrap?: "normal" | "break-word";
19 | outlineWidth?: number | string;
20 | outlineOffsetX?: number | string;
21 | outlineOffsetY?: number | string;
22 | outlineBlur?: number | string;
23 | outlineOpacity?: number;
24 | strokeWidth?: number | string;
25 | strokeOpacity?: number;
26 | fillOpacity?: number;
27 | debugSDF?: boolean;
28 | onSync?: (troika: { textRenderInfo: TextRenderInfo }) => void;
29 | text?: string;
30 | } & JSX.IntrinsicElements["mesh"];
31 |
32 | const InputText = React.forwardRef((props: Props, ref) => {
33 | const {
34 | fontSize = 0.07,
35 | anchorX = "left",
36 | anchorY = "middle",
37 | depthOffset = 0.5,
38 | color = "black",
39 | text,
40 | children,
41 | ...restProps
42 | } = props;
43 |
44 | const localRef = React.useRef(null);
45 | React.useImperativeHandle(ref, () => localRef.current);
46 | const stencil = useMask(1);
47 |
48 | return (
49 |
58 |
59 | {children}
60 | {text}
61 |
62 | );
63 | });
64 |
65 | InputText.displayName = "InputText";
66 |
67 | export { InputText };
68 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.0.0-development",
3 | "name": "r3f-form",
4 | "description": "An *accessible* webGL form component library for use with React Three Fiber",
5 | "type": "module",
6 | "module": "dist/index.js",
7 | "main": "dist/index.js",
8 | "types": "dist/index.d.ts",
9 | "publishConfig": {
10 | "access": "public"
11 | },
12 | "scripts": {
13 | "dev": "vite",
14 | "build": "tsc && vite build",
15 | "format": "prettier --write .",
16 | "format:check": "prettier --check .",
17 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
18 | "preview": "vite preview",
19 | "commit": "cz",
20 | "semantic-release": "semantic-release --branches main",
21 | "storybook": "storybook dev -p 6006",
22 | "build-storybook": "storybook build"
23 | },
24 | "author": "John Beresford",
25 | "license": "MIT",
26 | "devDependencies": {
27 | "@storybook/addon-essentials": "^7.6.17",
28 | "@storybook/addon-interactions": "^7.6.17",
29 | "@storybook/addon-links": "^7.6.17",
30 | "@storybook/addon-onboarding": "^1.0.11",
31 | "@storybook/blocks": "^7.6.17",
32 | "@storybook/jest": "^0.2.3",
33 | "@storybook/react": "^7.6.17",
34 | "@storybook/react-vite": "^7.6.17",
35 | "@storybook/test": "^7.6.17",
36 | "@storybook/testing-library": "^0.2.2",
37 | "@types/react": "^18.2.56",
38 | "@types/react-dom": "^18.2.19",
39 | "@types/three": "^0.155.1",
40 | "@typescript-eslint/eslint-plugin": "^7.0.2",
41 | "@typescript-eslint/parser": "^7.0.2",
42 | "@vitejs/plugin-react": "^4.2.1",
43 | "commitizen": "^4.3.0",
44 | "cz-conventional-changelog": "^3.3.0",
45 | "eslint": "^8.56.0",
46 | "eslint-config-prettier": "^9.0.0",
47 | "eslint-plugin-prettier": "^5.0.0",
48 | "eslint-plugin-react": "^7.31.11",
49 | "eslint-plugin-react-hooks": "^4.6.0",
50 | "eslint-plugin-react-refresh": "^0.4.5",
51 | "eslint-plugin-storybook": "^0.8.0",
52 | "prettier": "^3.0.0",
53 | "semantic-release": "^23.0.2",
54 | "storybook": "^7.6.17",
55 | "tslib": "^2.6.2",
56 | "typescript": "^5.2.2",
57 | "vite": "^5.1.4",
58 | "vite-plugin-dts": "^3.7.3"
59 | },
60 | "peerDependencies": {
61 | "@react-three/drei": ">=9.0",
62 | "@react-three/fiber": ">=8.0",
63 | "react": ">=18.0",
64 | "troika-three-text": "^0.49.0",
65 | "react-dom": ">=18.0",
66 | "three": ">=0.146.0"
67 | },
68 | "repository": {
69 | "type": "git",
70 | "url": "https://github.com/JMBeresford/r3f-form.git"
71 | },
72 | "config": {
73 | "commitizen": {
74 | "path": "./node_modules/cz-conventional-changelog"
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/.storybook/common.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Canvas, Color, useFrame, useThree } from "@react-three/fiber";
3 | import { Environment, Float } from "@react-three/drei";
4 | import { damp, randFloat, randInt, seededRandom } from "three/src/math/MathUtils";
5 | import { Group } from "three";
6 |
7 | const Shapes = () => {
8 | let n = 7;
9 | const ref = React.useRef(null);
10 |
11 | useFrame((_, delta) => {
12 | if (ref.current && ref.current.rotation.y) {
13 | ref.current.rotation.y += delta * 10;
14 | }
15 | });
16 |
17 | return (
18 | <>
19 |
20 | {Array.from({ length: n }).map((_, idx) => {
21 | const r = randFloat(0.35, 0.8);
22 | const b = randFloat(0.35, 0.8);
23 | const g = randFloat(0.35, 0.8);
24 | const color: Color = [r, g, b];
25 |
26 | const pos: [number, number, number] = [
27 | randFloat(0.2, 1.2) * (Math.random() < 0.5 ? 1 : -1),
28 | randFloat(0.2, 0.8) * (Math.random() < 0.5 ? 1 : -1),
29 | randFloat(0.2, 0.5) * (Math.random() < 0.5 ? 1 : -1),
30 | ];
31 |
32 | const rot: [number, number, number] = [
33 | randFloat(-Math.PI * 2, Math.PI * 2),
34 | randFloat(-Math.PI * 2, Math.PI * 2),
35 | randFloat(-Math.PI * 2, Math.PI * 2),
36 | ];
37 |
38 | const geometries = [
39 | ,
40 | ,
41 | ,
42 | ];
43 |
44 | return (
45 |
46 |
47 | {geometries[randInt(0, geometries.length - 1)]}
48 |
49 |
50 |
51 | );
52 | })}
53 |
54 | >
55 | );
56 | };
57 |
58 | const Rig = () => {
59 | const camera = useThree((s) => s.camera);
60 |
61 | useFrame(({ mouse }, delta) => {
62 | let x = mouse.x * 0.1;
63 | let y = mouse.y * 0.1;
64 |
65 | camera.rotation.x = damp(camera.rotation.x, y, 12, delta);
66 | camera.rotation.y = damp(camera.rotation.y, -x, 12, delta);
67 | });
68 | return <>>;
69 | };
70 |
71 | const Scene = ({ lightColor = "red", children }) => (
72 |
81 | );
82 |
83 | export { Scene };
84 |
--------------------------------------------------------------------------------
/src/Submit/Submit.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as ReactDOM from "react-dom/client";
3 | import { Text, useCursor } from "@react-three/drei";
4 | import { Color, ThreeEvent, useThree } from "@react-three/fiber";
5 | import { useFormContext } from "../Form";
6 |
7 | type ButtonProps = {
8 | value?: string;
9 | fontSize?: number;
10 | width?: number;
11 | height?: number;
12 | color?: Color;
13 | backgroundColor?: Color;
14 | };
15 |
16 | export type Props = ButtonProps &
17 | Pick &
18 | Omit;
19 |
20 | const Button = (props: ButtonProps) => {
21 | const {
22 | backgroundColor,
23 | width = 1.5,
24 | height = 0.1325,
25 | fontSize = 0.0825,
26 | color = "black",
27 | value,
28 | } = props;
29 |
30 | return (
31 |
32 |
33 | {value}
34 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 | };
42 |
43 | const Submit = React.forwardRef((props: Props, ref: React.ForwardedRef) => {
44 | const { value, children, position, rotation, scale, ...restProps } = props;
45 |
46 | const root = React.useRef(null);
47 | const formNode = useFormContext();
48 | const events = useThree((s) => s.events);
49 | const gl = useThree((s) => s.gl);
50 | const target = (formNode?.current || events.connected || gl.domElement.parentNode) as HTMLElement;
51 |
52 | const [domEl] = React.useState(() => document.createElement("div"));
53 | const [hovered, setHovered] = React.useState(false);
54 |
55 | useCursor(hovered);
56 |
57 | const handleClick = React.useCallback(
58 | (e: ThreeEvent) => {
59 | e.stopPropagation();
60 |
61 | if (formNode.current?.requestSubmit) {
62 | formNode.current.requestSubmit();
63 | } else {
64 | formNode.current?.submit();
65 | }
66 | },
67 | [formNode]
68 | );
69 |
70 | React.useLayoutEffect(() => {
71 | root.current = ReactDOM.createRoot(domEl);
72 | const curRoot = root.current;
73 |
74 | target?.appendChild(domEl);
75 |
76 | return () => {
77 | target?.removeChild(domEl);
78 | curRoot.unmount();
79 | };
80 | }, [domEl, target]);
81 |
82 | React.useLayoutEffect(() => {
83 | root.current?.render(
84 |
97 | );
98 | });
99 |
100 | return (
101 | <>
102 | setHovered(true)}
105 | onPointerLeave={() => setHovered(false)}
106 | position={position}
107 | rotation={rotation}
108 | scale={scale}
109 | >
110 | {children ? children : }
111 |
112 | >
113 | );
114 | });
115 |
116 | Submit.displayName = "InputSubmit";
117 |
118 | export { Submit };
119 |
--------------------------------------------------------------------------------
/src/troika-three-text.d.ts:
--------------------------------------------------------------------------------
1 | declare module "troika-three-text" {
2 | declare type SelectionRect = {
3 | bottom: number;
4 | top: number;
5 | left: number;
6 | right: number;
7 | };
8 |
9 | declare type AnchorXValue = number | "left" | "center" | "right";
10 |
11 | declare type AnchorYValue =
12 | | number
13 | | "top"
14 | | "top-baseline"
15 | | "top-cap"
16 | | "top-ex"
17 | | "middle"
18 | | "bottom-baseline"
19 | | "bottom";
20 |
21 | declare type TypesetParams = {
22 | text?: string;
23 | fontSize?: number;
24 | font?: unknown;
25 | lang?: string;
26 | sdfGlyphSize?: number;
27 | fontWeight?: number | "normal" | "bold";
28 | fontStyle?: "normal" | "italic";
29 | letterSpacing?: number;
30 | lineHeight?: number | "normal";
31 | maxWidth?: number;
32 | direction?: "ltr" | "rtl";
33 | textAlign?: "left" | "right" | "center" | "justify";
34 | textIndent?: number;
35 | whiteSpace?: "normal" | "nowrap";
36 | overflowWrap?: "normal" | "break-word";
37 | anchorX?: AnchorXValue;
38 | anchorY?: AnchorYValue;
39 | metricsOnly?: boolean;
40 | unicodeFontsURL?: string;
41 | preResolveFonts?: unknown;
42 | includeCaretPositions?: boolean;
43 | chunkedBoundsSize?: number;
44 | colorRanges?: unknown;
45 | };
46 |
47 | declare type BoundingRect = [minX: number, minY: number, maxX: number, maxY: number];
48 |
49 | declare type ChunkedBounds = {
50 | start: number;
51 | end: number;
52 | rect: BoundingRect;
53 | };
54 |
55 | declare type TextRenderInfo = {
56 | parameters?: TypesetParams;
57 | sdfTexture?: Texture;
58 | sdfGlyphSize?: number;
59 | sdfExponent?: number;
60 |
61 | // List of [minX, minY, maxX, maxY] quad bounds for each glyph.
62 | glyphBounds?: number[];
63 |
64 | // List holding each glyph's index in the SDF atlas.
65 | glyphAtlasIndices?: number[];
66 |
67 | // List holding each glyph's [r, g, b] color, if `colorRanges` was supplied.
68 | glyphColors?: number[];
69 |
70 | // A list of caret positions for all characters in the string; each is
71 | // four elements: the starting X, the ending X, the bottom Y, and the top Y for the caret.
72 | caretPositions?: number[];
73 |
74 | // An appropriate height for all selection carets.
75 | caretHeight?: number;
76 |
77 | // The font's ascender metric.
78 | ascender?: number;
79 |
80 | // The font's descender metric.
81 | descender?: number;
82 |
83 | // The font's cap height metric, based on the height of Latin capital letters.
84 | capHeight?: number;
85 |
86 | // The font's x height metric, based on the height of Latin lowercase letters.
87 | xHeight?: number;
88 |
89 | // The final computed lineHeight measurement.
90 | lineHeight?: number;
91 |
92 | // The y position of the top line's baseline.
93 | topBaseline?: number;
94 |
95 | // The total [minX, minY, maxX, maxY] rect of the whole text block;
96 | // this can include extra vertical space beyond the visible glyphs due to lineHeight.
97 | // Equivalent to the dimensions of a block-level text element in CSS.
98 | blockBounds?: number[];
99 |
100 | // The total [minX, minY, maxX, maxY] rect of the whole text block;
101 | // unlike `blockBounds` this is tightly wrapped to the visible glyph paths.
102 | // Equivalent to the dimensions of an inline text element in CSS.
103 | visibleBounds?: number[];
104 |
105 | // List of bounding rects for each consecutive set of N glyphs,
106 | // in the format `{start:N, end:N, rect:[minX, minY, maxX, maxY]}`.
107 | chunkedBounds?: ChunkedBounds[];
108 |
109 | // Timing info for various parts of the rendering logic including SDF
110 | // generation, typesetting, etc.
111 | timings?: object;
112 | };
113 | declare function getCaretAtPoint(
114 | troikaText: TextRenderInfo,
115 | x: number,
116 | y: number
117 | ): { charIndex: number };
118 | declare function getSelectionRects(
119 | troikaText: TextRenderInfo,
120 | start: number,
121 | end: number
122 | ): SelectionRect[];
123 | }
124 |
--------------------------------------------------------------------------------
/.storybook/stories/Form.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Form, Input, Textarea, Label, Submit, InputText } from "../../src";
3 | import type { Meta, StoryObj } from "@storybook/react";
4 | import { action } from "@storybook/addon-actions";
5 | import { Group } from "three";
6 | import { damp } from "../../src/utils";
7 | import { Text } from "@react-three/drei";
8 | import { useFrame } from "@react-three/fiber";
9 |
10 | const meta: Meta = {
11 | component: Default,
12 | title: "Form",
13 | };
14 |
15 | export default meta;
16 | type Story = StoryObj;
17 |
18 | const handleSubmit = (e: React.FormEvent) => {
19 | e.preventDefault();
20 | const data = new FormData(e.target as HTMLFormElement);
21 |
22 | let res = "";
23 |
24 | for (let [k, v] of data.entries()) {
25 | if (!v) return;
26 | res += `${k}: ${v}\n`;
27 | }
28 |
29 | window.alert(res);
30 | return res;
31 | };
32 |
33 | const submitAction = (event) => {
34 | action("form submitted")(handleSubmit(event));
35 | };
36 |
37 | function Default() {
38 | const ref = React.useRef(null);
39 |
40 | return (
41 |
42 |
53 |
54 | );
55 | }
56 |
57 | function Button() {
58 | const btnRef = React.useRef(null);
59 |
60 | const [hovered, setHovered] = React.useState(false);
61 | const [submitting, setSubmitting] = React.useState(false);
62 |
63 | useFrame((_, delta) => {
64 | if (btnRef.current) {
65 | let low = submitting ? 0.2 : 0.8;
66 |
67 | let y = hovered ? low : 1;
68 | btnRef.current.scale.y = damp(btnRef.current.scale.y, y, 12, delta);
69 |
70 | if (btnRef.current.scale.y === 0.2) {
71 | setSubmitting(false);
72 | }
73 | }
74 | });
75 |
76 | return (
77 | setHovered(true)}
81 | onPointerLeave={() => {
82 | setHovered(false);
83 | setSubmitting(false);
84 | }}
85 | onPointerDown={() => setSubmitting(true)}
86 | onPointerUp={() => setSubmitting(false)}
87 | >
88 |
96 | Login
97 |
98 |
99 |
100 |
101 |
102 |
103 | );
104 | }
105 |
106 | function WithCustomButton() {
107 | const ref = React.useRef(null);
108 |
109 | return (
110 |
111 |
126 |
127 | );
128 | }
129 |
130 | function WithTextArea() {
131 | const ref = React.useRef(null);
132 |
133 | return (
134 |
135 |
154 |
155 | );
156 | }
157 |
158 | export const DefaultStory: Story = {
159 | name: "Default",
160 | render: () => ,
161 | };
162 |
163 | export const CustomButtonStory: Story = {
164 | name: "With Custom Button",
165 | render: () => ,
166 | };
167 |
168 | export const WithTextAreaStory: Story = {
169 | name: "With TextArea",
170 | render: () => ,
171 | };
172 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # r3f-form
2 |
3 | ### [View examples](https://jmberesford.github.io/r3f-form/?path=/story/form--default-submit-button)
4 |
5 | > ## WARNING: UNDER HEAVY DEVELOPMENT
6 | >
7 | > Each release will aim to be a fully functional and usable release,
8 | > but breaking API changes WILL be likely for the forseeable future.
9 |
10 | ## A webGL form component for use with React Three Fiber
11 |
12 | 
13 |
14 | This package aims to create a fully-functional and **accessible** ``
15 | components that can be used within the [@react-three/fiber](https://github.com/pmndrs/react-three-fiber)
16 | ecosystem. Ultimately, the goal is to have fully functioning HTML `
62 | );
63 | }
64 | ```
65 |
66 | > Note that each element in the form will require a `name` prop in order to be picked up in submission, just like in a normal DOM form element
67 |
68 | The relevant inputs will be bound to respective DOM elements under the hood, and be rendered into the 3D scene like so:
69 |
70 | 
71 |
72 | You can define submission behavior just like with any old HTML `;
77 |
78 | // or handle it with a callback
79 | const handleSubmit = (e: FormEvent) => {
80 | e.preventDefault();
81 |
82 | const data = new FormData(e.target);
83 |
84 | for (let [name, value] of data.entries()) {
85 | console.log(`${name}: ${value}`);
86 | }
87 | };
88 |
89 | ;
90 | ```
91 |
92 | ## Inputs
93 |
94 | An editable text-box bound to an DOM `` and represented in the webGL canvas.
95 |
96 | ```ts
97 | type Props = {
98 | type?: "text" | "password";
99 |
100 | /** width of the container */
101 | width?: number;
102 | backgroundColor?: Color;
103 | selectionColor?: Color;
104 | backgroundOpacity?: number;
105 |
106 | /** [left/right , top/bottom] in THREE units, respectively
107 | *
108 | * note that height is implicitly defined by the capHeight of the rendered
109 | * text. The cap height is dependant on both the `textProps.font` being used and the
110 | * `textProps.fontSize` value
111 | */
112 | padding?: Vector2;
113 | cursorWidth?: number;
114 |
115 | /** 3D transformations */
116 | position: Vector3;
117 | rotation: Euler;
118 | scale: Vector3;
119 |
120 | // And ALL props available to DOM s
121 | };
122 | ```
123 |
124 | Create a basic input field like so:
125 |
126 | ```tsx
127 | import { Input, Label } from "r3f-form";
128 |
129 | export function MyInput() {
130 | return (
131 | <>
132 |
133 |
134 | >
135 | );
136 | }
137 | ```
138 |
139 | 
140 |
141 | You can access the value of the input via the `onChange` callback prop:
142 |
143 | > The callback is passed the `ChangeEvent` object from the underlying HTML ``s
144 | > change event on every change.
145 | >
146 | > Read more about this event [here](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event)
147 |
148 | ```tsx
149 | import { Input, Label } from "r3f-form";
150 |
151 | export function MyInput() {
152 | const [username, setUsername] = React.useState("");
153 | // username will always contain the current value
154 |
155 | function handleChange(e) {
156 | setUsername(ev.target.value);
157 | }
158 |
159 | return (
160 | <>
161 |
162 |
163 | >
164 | );
165 | }
166 | ```
167 |
168 | You can also create password inputs:
169 |
170 | ```tsx
171 | import { Input, Label } from "r3f-form";
172 |
173 | export function MyPassword() {
174 | return (
175 | <>
176 |
177 |
178 | >
179 | );
180 | }
181 | ```
182 |
183 | 
184 |
185 | Add custom padding to the text container:
186 |
187 | ```tsx
188 | import { Input, Label } from "r3f-form";
189 |
190 | /*
191 | * padding: [horizontal padding, vertical padding] in THREE units
192 | */
193 |
194 | export function MyInput() {
195 | return (
196 | <>
197 |
198 |
199 | >
200 | );
201 | }
202 | ```
203 |
204 | 
205 |
206 | ---
207 |
208 | ## Textarea
209 |
210 | ```ts
211 | type Props = {
212 | /** width of the container */
213 | width?: number;
214 |
215 | backgroundColor?: Color;
216 | backgroundOpacity?: number;
217 | selectionColor?: Color;
218 |
219 | /** [left/right , top/bottom] in THREE units, respectively
220 | *
221 | * note that height is implicitly defined by the capHeight of the rendered
222 | * text. The cap height is dependant on both the `textProps.font` being used
223 | * and the `textProps.fontSize` value
224 | */
225 | padding?: Vector2;
226 | cursorWidth?: number;
227 |
228 | /** 3D transformations */
229 | position: Vector3;
230 | rotation: Euler;
231 | scale: Vector3;
232 |
233 | // And ALL props available to DOM