Step-by-Step Guide: Build Your First Custom CRM Card in HubSpot (Part 2)

Adriane Grunenberg
Adriane Grunenberg
Step-by-Step Guide: Build Your First Custom CRM Card in HubSpot (Part 2)
12:54

Ready to take your HubSpot customization skills to the next level? In Part 1 of our guide, we laid the foundation by understanding the difference between custom and CRM cards and installing a sample custom card. Now, it's time to roll up your sleeves and dive deeper into the world of custom cards in HubSpot. In Part 2, we'll focus on harnessing the power of React, a powerhouse JavaScript library, to build custom cards that perfectly align with your business objectives


Plus, you’ll learn how to set up your local environment, so you'll have a safe space to test and refine your creations before unleashing them to the world. Get ready to unlock the full potential of HubSpot customization – let's dive in!

 

Building Your Own Custom Card

With your sample card set up, you’re ready to start building a custom card from scratch. First, you’ll utilize the VS Code-terminal to create all the folders and files for the initial configuration. These files will be blank initially.  Later on, you’ll revisit them to input the code in subsequent steps.

 

Define the structure of your custom card

  1. Create a new folder on your computer desktop and name it something like “customCard2” or whatever. 

 

Attention: If your folder names contain spaces, VS Code cannot process them and will consistently generate a “too many arguments" error. Therefore, it's advisable to avoid using spaces in names altogether.

 

Here is the corrected version of your list:

2. Open up your newly created folder in VS Code and initialize the HubSpot CLI:

  • …either by using the HubSpot extension in VS Code like you did before. Then, you may skip the next steps and continue with step 3.
  • …or by running the following command in the terminal of VS Code, which asks you to open your HubSpot account and copy your personal access key:

hs init

 

    • Hit “y” for “Yes” and “Enter” on your keyboard.
  • Open your HubSpot test account.
  • Click on “Show” under your hidden personal access key:

 

00 Getting your Personal Access Key in HubSpot

 

  • Copy your revealed personal access key.
  • Paste your key in to your VS Code terminal and hit “Enter”.
  • Accept the default name for the account in the HubSpot CLI by hitting “Enter” again.

 

3. Next, run the following command to generate a new project that will serve as the container for or your application within your account:

hs project create

 

4. The HubSpot CLI now prompts you with a set of inquiries, covering the project's name, location, and template preferences:

    • Name your project “customCard2
    • Opt for the default location
    • Select no template

These actions will generate a new folder named “src” along with a hsproject.json file.

 

01 Create HubSpot Project in VS Code

 

5. Add the package.json file – optional but highly recommended because this will simplify your life slightly by eliminating the need to navigate to other directories for installing dependencies:

  • Head to the main folder in your VS Code terminal >again and run the following commands, one after another:

cd customCard2

 

For Mac-users:

touch package.json

 

For Windows-users:

New-Item package.json

 

02 Create New Item in VS Code

 

6. Add the structure for a private app, which is necessary for UI extensions to function and can be used for multiple custom cards, by running the following commands, one after another, still in the VS Code terminal of your main folder:

cd src

 

mkdir app

 

cd app

 

For Mac-users:

touch app.json

 

For Windows-users:

New-Item app.json

 

7. Next, create the extensions folder and the files “custom-card.json”, “CustomCard.jsx or CustomCard.tsx” and “package.json” (can be shared among multiple cards) by heading to the app folder and running the following commands in your terminal:

mkdir extensions

 

cd extensions

 

For Mac-users:

touch associated-deals.json

 

For Windows-users:

New-Item associated-deals.json

 

For Mac-users:

touch AssociatedDeals.jsx

 

For Windows-users:

New-Item AssociatedDeals.jsx

 

For Mac-users:

touch package.json

 

For Windows-users:

New-Item package.json

 

03 Create extension files in VS Code

 

8. If you wish to delve further into your CRM or interact with data beyond, serverless functions are the logical next step. For example, you can use a serverless function to interact with a HubDB table hosted on another HubSpot account.
To store serverless functions you need to create another folder(“app.functions”) and additional three files (“functionName.json”, “serverless.json” and “package.json”).

To do so navigate out of the extensions folder and back into the app folder where you run the following commands in your terminal, one after another:

cd ..

 

cd app.functions

 

For Mac-users:

touch thanksKyle.js

 

For Windows-users:

New-Item thanksKyle.js

 

For Mac-users:

touch serverless.json

 

For Windows-users:

New-Item serverless.json

 

For Mac-users:

touch package.json

 

For Windows-users:

New-Item package.json

 

For Mac-users:

touch .env

 

For Windows-users:

New-Item .env

 

04 Create app.functions in VS Code

 

Add codes to your custom card

Let’s pause here for a minute and celebrate: The structure for your custom card is set up – congratulations on coming so far! 

9. Now, let’s add some content with the help of the optional package.json file from step 5:

  • To execute the scripts in the extensions and app.functions package.json file, navigate to the empty package.json file situated at the root level of your project in VS code, and append the following lines:

 

Attention: Especially for future use, you'll need to customize the code slightly as shown in the red-highlighted phrases below.

 

{

"name": "customcard2",

"version": "0.1.0",

"scripts": {

"postinstall": "cd ./src/app/extensions/ && npm install && cd ../app.functions && npm install"

},

"author": {

"name": "your first name your last name",

"email": "yourname@youremailprovider.com"

},

"license": "MIT"

}

 

 

05 Paste code in package.json file

 

10. Next, create the necessary private app by filling in the data in the app.json file located in the app folder running the following code in your terminal:

{ "name": "Myfirstcustomcard",

"description": "The description of my first custom card",

"uid": "my-first-custom-cards-uid-which-cannot-be-changed-afterwards"

"scopes": [

"crm.objects.contacts.read" ],

"public": false,

"extensions": {

"crm": {

"cards": [ { "file": "extensions/associated-deals.json"}]

} } }

 

06 Paste code in app.json file

 

11. Then, you are ready to input the codes for the custom card. Add the following lines to the following files located in the extensions folder:
    • associated-deals.json:

{

"type": "crm-card",

"data": {

"title": "My first custom card",

"uid": "my-first-custom-cards-uid-which-cannot-be-changed-afterwards",

"location": "crm.record.tab",

"module": { "file": "AssociatedDeals.jsx" },

"objectTypes": [ { "name": "contacts" } ]

} }

 

07 Paste code in associated-deals.json file

 

  • AssociatedDeals.jsx (the react file):

import Reactfrom "react";
import { hubspot }from "@hubspot/ui-extensions";

hubspot.extend(() => (
  <Extension


  />
));
const Extension =() => { }

 

08 Paste code in AssociatedDeals.jsx file

 

  • package.json (for the front-end):

{

"name": "myfirstcustomcard-extension",

"version": "0.1.0",

"author": "Your First Name Your Last Name",

"license": "MIT",

"scripts": { "dev": "hs project dev" },

"dependencies": { "@hubspot/ui-extensions": "latest", "react": "^18.2.0" }

}

 

09 Paste code in package.json file

 

12. Now, let’s input the codes for the serverless functions. Add the following lines to the following files located in the app.functions folder:

    • package.json:

{

"name": "hubdb_updater",

"version": "0.1.0",

"author": "Your First Name Your Last Name",

"license": "MIT", "dependencies":

{ "axios": "^0.27.2" }

}

 

10 Paste code in package.json file

 


const axios = require('axios');

exports.main = async () => {}

 

11 Paste code in thanksKyle.js file

 

  • serverless.json (for the back-end):

{

"appFunctions": {

"jepsonupdater":

{ "file": "thanksKyle.js",

"secrets": []

} } }

 

12 Paste code in serverless.json file

 

13. Finally, save your project (via “File” → “Save” or the keyboard shortcut “Control + S”) and you’re ready to upload your custom card to your HubSpot test account:

    • Return to the terminal and ensure you're in the root directory, which is the one containing the hsproject.json file:

 

 

  • Once there, execute the following commands one after another:

npm install

 

hs project upload

 

  • Check if everything proceeded smoothly, by logging in to your test account and adding this card to contact records.

 

 

Specify the functions of your custom card

Once you have your card's basic structure in place, it's time to add functionality. This includes handling user interactions, fetching data from external sources, and updating the card's display accordingly. You'll use JavaScript and HubSpot APIs to implement the desired functionality. Again: don't worry if you're not familiar with all of these – we'll walk you through each step of the process.

Initially, you'll start your local development server to facilitate the creation of your cards. Through local development, you can confidently build and test your card without worrying about affecting the production card until you upload your changes. 

  • Go to your terminal in VS Code again where you’ve left and run the following command:

hs project dev

 

  • Since there are no sandboxes in HubSpot test accounts, choose “Test on this production account” instead and press “Enter” on your keyboard.
  • Open your HubSpot test account and go to a contact record displayed with a label above, indicating that you're actively developing this custom card. This visibility is exclusive to you and not visible to anyone else:

 

13 Developing locally in HubSpot

 

  • Prior to implementing functionality into the custom card, let's enhance the organization of your React components:
  • Go to the extensions folder in VS Code, right-click on it, select “New Folder…” in the context menu and name it “components”:

 

14 Adding new folder within extensions folder in VS Code

 

  • Within this folder, create the following 5 files:
    • Clicker.jsx (“View More Button”)
    • DealPanel.jsx (the outer level of the panel)
    • DealPanelInner.jsx (where you can see the data that you receive for the panel)
    • Layout.jsx (controls the layout for the component)
    • Stats.jsx (the component that shows the data before you open the panel)

 

15 Adding components files in VS Code

 

18. Now finish building your custom card by adding codes to the following three  components you’ve just created:

    • Layout.jsx:

// first we need to import the Flex component from the ui-extensions package

import {Flex} from '@hubspot/ui-extensions';

// then we will create a functional component that takes in two props, stats and clicker

export const Layout = ({ stats, clicker }) =>

{ return (    

<Flex direction="row" align='start'>
            <Flex flex="1" align="start">
            {stats}
            </Flex>
            <Flex flex="2" alignSelf="center">
            {clicker}
            </Flex>
        </Flex>)}

 

16 Pasting code in layout.jsx file

 

  • Stats.jsx:

// first we need to import the Flex component from the ui-extensions package
import { CrmStatistics } from '@hubspot/ui-extensions/crm';
 
// then we will create a functional component that takes in two props, statTitle and operator
export const Stats = ({ statTitle, operator }) => {
  return (
    <CrmStatistics
      objectTypeId="0-3"
      statistics={[
        {
          label: statTitle,
          statisticType: 'COUNT',
          propertyName: 'amount',
          // The filters below narrow the fetched 
          // deals by the following criteria:
          // - Amount must be >= 1,000
          // - Deal must not be closed
          filterGroups: [
            {
              filters: [
                {
                  operator: operator,
                  property: 'amount',
                  value: 1000,
                },
                {
                  operator: 'NOT_IN',
                  property: 'dealstage',
                  values: ['closedwon', 'closedlost'],
                },
              ],
            },
          ],
        },
      ]}
    />
  );
};

 

17 Pasting code in stats.jsx file

 

  • Clicker.jsx:

import { Button } from "@hubspot/ui-extensions";
 
export const Clicker = ({ panelId }) => {
  panelId = panelId;
  return (
    <Button onClick={(event, reactions) => { reactions.openPanel(panelId) }}>
      View More
    </Button>
  )
}

 

18 Pasting code in clicker.jsx file

 

19. Next, open the parent component “AssociatedDeals.jsx” and overwrite the existing code with the following one:

import React from "react";
import { hubspot } from "@hubspot/ui-extensions";
import { Layout } from "./components/Layout";
import { Stats } from "./components/Stats";
import { Clicker } from "./components/Clicker";
 
hubspot.extend(() => (
  <Extension
 
  />
));
 
const Extension = () => {
  return (
    <>
      <Layout 
 stats={<Stats statTitle={'Count of deals over 1000 dollars'} operator={'GT'} />} 
 clicker={<Clicker panelId={"big-panel"} />} />
      <Layout 
 stats={<Stats statTitle={'Count of deals under 1000 dollars'} operator={'LT'} />} 
 clicker={<Clicker panelId={"little-panel"} />} />
    </>
  )
}

 

19 Rewriting code of AssociatedDeals.jsx file

 

Meanwhile, content is displaying in your custom card, but it's not yet functional:

 

20 Still functionless content appears at front end

 

Let's continue to make it useful for you:

20. Create the panel of the custom card which will consist of the files DealPanel.jsx and DealPanelInner.jsx:

  • Open “DealPanel.jsx” and copy / paste the following code:

import {
  Panel,
  PanelBody,
  PanelFooter,
  PanelSection,
} from '@hubspot/ui-extensions';
 
export const DealPanel = ({ paneltitle, panelId, children, customFooter }) => {
  return (
    <>
      <Panel title={paneltitle} id={panelId}>
        <PanelBody>
          <PanelSection>
            {children}
          </PanelSection>
        </PanelBody>
        <PanelFooter>
          {customFooter || "Footer text"}
        </PanelFooter>
      </Panel>
    </>
  )
}

 

21 Pasting code in DealPanel.jsx file

 

  • Open “DealPanelInner.jsx” and copy / paste the following code:

 import { Text, Flex } from '@hubspot/ui-extensions';
import { CrmAssociationTable } from '@hubspot/ui-extensions/crm';
 
export const DealPanelInner = ({ panelSubtitle, operator }) => {
  return (
    <>
      <Flex direction={'column'} gap={'lg'}>
        <Text variant="microcopy">
          This example is a card for contact records to display high-level associated deal information in a table.
        </Text>
        <Text format=>{panelSubtitle}</Text>
        <CrmAssociationTable
          objectTypeId="0-3"
          propertyColumns={[
            'dealname',
            'hubspot_owner_id',
            'amount',
            'dealstage',
          ]}
          quickFilterProperties={['hubspot_owner_id', 'dealstage', 'amount']}
          pageSize={10}
          preFilters={[
            {
              operator: 'NOT_IN',
              property: 'dealstage',
              values: ['closedwon', 'closedlost'],
            },
            {
              operator: operator,
              property: 'amount',
              value: 1000,
            },
          ]}
          sort={[
            {
              direction: 1,
              columnName: 'amount',
            },
          ]}
          searchable={true}
          pagination={true}
        />
      </Flex>
    </>
  )
}


 

22 Pasting code in DealPanelInner.jsx file

 

21. Finally, open the parent component “AssociatedDeals.jsx” again and overwrite the existing code with the following one:

import React from "react";
import { hubspot } from "@hubspot/ui-extensions";
import { Layout } from "./components/Layout";
import { Stats } from "./components/Stats";
import { Clicker } from "./components/Clicker";
import { DealPanel } from "./components/DealPanel";
import { DealPanelInner } from "./components/DealPanelInner";
 
hubspot.extend(() => (
  <Extension
 
  />
));
 
const Extension = () => {
  return (
    <>
      <DealPanel paneltitle={'More than 1000 dollars'} panelId={"big-panel"}>
        <DealPanelInner panelSubtitle="Check out these big deals" operator={'GT'} />
      </DealPanel>
      <DealPanel paneltitle={"Less than 1000 dollars"} panelId={"little-panel"}>
        <DealPanelInner panelSubtitle="Check out these little deals" operator={'LT'} />
      </DealPanel>
      <Layout
        stats={<Stats statTitle={'Count of deals over 1000 dollars'} operator={'GT'} />}
        clicker={<Clicker panelId={"big-panel"} />} />
      <Layout
        stats={<Stats statTitle={'Count of deals under 1000 dollars'} operator={'LT'} />}
        clicker={<Clicker panelId={"little-panel"} />} />
    </>
  )}

 

23 Rewriting code of AssociatedDeals.jsx file

 

Don’t forget to save your project (via “File” → “Save” or the keyboard shortcut “Control + S”)!

Congratulations again! At this stage, you have built a fully operational custom card capable of displaying high- and low-level associated deal information for a contact in a table

Check it out by navigating to a contact record in your test-account and take a look at your custom card:

 

24 Testing functionality of Custom Card in HubSpot

 

Now you have two options: 

Either, if you prefer to focus on completing your existing custom card, you can skip the next chapter and continue with “Testing Your Custom Card.”

Or, if you’re feeling up to the challenge of enhancing your CRM card further, let’s integrate a button that utilizes a serverless function to update a HubDB table.

 

Bonus: Adding a button to your custom card by using a serverless function 

Let's explore a bonus that adds an extra layer of functionality to your CRM card: serverless functions. While not essential for the basic functionality of a custom card, serverless functions can significantly enhance its capabilities by enabling dynamic interactions, data processing, and seamless integration with external resources.

For this example, you'll be utilizing a HubDB table graciously provided by HubSpot, along with the sample website thanks.kyle.team. So, let's express our gratitude to Kyle and Dennis from HubSpot by constructing this serverless function together.

22. open the parent component “AssociatedDeals.jsx” again and add a button component by replacing the existing code with the following one:

import React from "react";
import { hubspot, Button } from "@hubspot/ui-extensions";
import { Layout } from "./components/Layout";
import { Stats } from "./components/Stats";
import { Clicker } from "./components/Clicker";
import { DealPanel } from "./components/DealPanel";
import { DealPanelInner } from "./components/DealPanelInner";
 
hubspot.extend(({ runServerlessFunction }) => (
  <Extension
    runServerless={runServerlessFunction}
  />
));
 
const Extension = ({ runServerless }) => {
  const handleClick = async () => {
    const { response } = await runServerless({ name: "jepsonupdater" });
  };
  return (
    <>
      <DealPanel paneltitle={'More than 1000 dollars'} panelId={"big-panel"}>
        <DealPanelInner panelSubtitle="Check out these big deals" operator={'GT'} />
      </DealPanel>
      <DealPanel paneltitle={"Less than 1000 dollars"} panelId={"little-panel"}>
        <DealPanelInner panelSubtitle="Check out these little deals" operator={'LT'} />
      </DealPanel>
      <Layout stats={<Stats statTitle={'Count of deals over 1000 dollar'} operator={'GT'} />} clicker={<Clicker panelId={"big-panel"} />} />
      <Layout stats={<Stats statTitle={'Count of deals under 1000 dollar'} operator={'LT'} />} clicker={<Clicker panelId={"little-panel"} />} />
      <Button onClick={handleClick}>
        Jepson Button
      </Button>
    </>
  )
}

 

25 Rewriting code of AssociatedDeals.jsx file

 

Now, on the front end, you've implemented a button click that activates a serverless function called "jepsonupdater":

26 Adding Button to Custom Card in HubSpot

 

You may remember that you specified the name of the serverless function in your serverless.json file here. In order for this serverless function to make an authenticated API call, you need to store the authentication method in a so-called “secret”:

23. Open the serverless.json file located in the app.functions folder and overwrite the existing code with the following one:

{
  "appFunctions": {
    "jepsonupdater": {
      "file": "thanksKyle.js",
      "secrets": [
        "hubdb"
      ]
    }
  }
}

 

27 Rewriting code of serverless.json

 

24. Now, to register the secret with HubSpot you need to stop watching the project by navigating to your terminal and hitting “Control” + “c” on your keyboard.

25. Then, add the following in your terminal and press “Enter”:

hs secrets add hubdb

 

26. You’ll be asked to enter a value for your secret. Copy & paste the following key in your terminal:

pat-na1-bccb209f-d999-4dcc-85ee-d2da7c875d62

 

Attention: you won’t see any asterisks or similar when pasting the key. Just hit “Enter” when you’re done.

 

28 Registered secret key with HubSpot

Great, you've successfully registered the token with HubSpot! However, due to security protocols, it won't be accessible for local development. Therefore, you must also include it in the .env file which you have created before in this step:

27. Head over to your terminal again and get the Development server back up and running by executing the following command:

hs project dev

 

28. Choose “Test on this production account” and press “Enter” on your keyboard.

29. Open the .env file in your app.functions folder and add the following:

hubdb=pat-na1-bccb209f-d999-4dcc-85ee-d2da7c875d62

 

29 Includin secret key in .env file

 

30. Don’t forget to save your project!

31. To create your serverless function, open up the thanksKyle.js file and replace the existing entry with the following one:

const axios = require('axios');
 
exports.main = async () => {
  // console.log("we are connected");
  const TABLE_ID = '14430732';
  const ROW_ID = '155745382401';
  const COLUMN_NAME = 'opacity';
  const Bearer = process.env['hubdb'];
  const axiosInstance = axios.create({
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${Bearer}`
    }
  });
 
  const getRow = async () => {
    const url = `https://api.hubapi.com/cms/v3/hubdb/tables/${TABLE_ID}/rows/${ROW_ID}`;
    try {
      const response = await axiosInstance.get(url);
      console.log('Successfully fetched row:', response.data);
      return response.data;
    } catch (error) {
      console.error('Error fetching row:', error);
      throw error;
    }
  };
 
  const updateRow = async (row) => {
    const url = `https://api.hubapi.com/cms/v3/hubdb/tables/${TABLE_ID}/rows/${ROW_ID}/draft`;
    try {
      const response = await axiosInstance.patch(url, { values: { [COLUMN_NAME]: parseFloat(row.values[COLUMN_NAME].toFixed(1)) + 0.10 } });
      console.log('Successfully updated row:', response.data);
      return response.data;
    } catch (error) {
      console.error('Error updating row:', error);
      throw error;
    }
  };
  const publishTable = async () => {
    const url = `https://api.hubapi.com/cms/v3/hubdb/tables/${TABLE_ID}/draft/publish`;
    console.log(`Attempting to publish table with ID ${TABLE_ID}`);
    try {
      const response = await axiosInstance.post(url);
      console.log('Successfully published table:', response.data);
      return response.data;
    } catch (error) {
      console.error('Error updating row:', error);
      throw error;
    }
  };
 
  try {
    const row = await getRow();
    const updatedRow = await updateRow(row);
    const publish = await publishTable(row);
 
    return { statusCode: 200, body: JSON.stringify(updatedRow) };
  } catch (error) {
    return { statusCode: 500, body: 'An error occurred' };
  }
};

 

30 Rewriting code of thanksKyle.js file

 

32. Finally, save your project (via “File” → “Save” or the keyboard shortcut “Control + S”) and go to your contact record in your HubSpot test account.

33.Test your card by clicking on the “Jepson Button.” You've succeeded if the image of Kyle on the page thanks.kyle.team becomes increasingly clearer with each click of the button until reaching full opacity:

 

Note: You may notice that when accessing the public landing page, Kyle is already fully visible due to someone successfully testing the serverless function before and not resetting the image to its original settings. In that case, another challenge awaits you: Add a reset button to your custom card so that which each click the image's transparency returns to 100%.

 

Testing Your Custom Card

After completing the construction of your custom cards, we’re sure you’re highly eager to deploy and utilize it. However, prior to deployment, it's crucial to allocate sufficient time to the next step: testing. 

Thorough testing of your custom card is imperative to verify its functionality before releasing it into production. Remain within the local development environment to conduct comprehensive testing across different scenarios, ensuring that the card performs as intended. Resolve any encountered issues or bugs during the testing phase before proceeding with deployment.

Trust us, investing time in thorough testing now will save you time and trouble in the long run, so it’s better not to neglect it!

 

Deploying Your Custom Card

Once you're confident that your custom card is functioning correctly, it's time to deploy it to your HubSpot account. That’s when you have to execute the upload command one final time to push your changes. So run the following code in your VS Code terminal:

hs project upload

 

Achievement Unlocked: 1st Custom Card Created

And there you have it – your first HubSpot custom card! By mastering the fundamentals of custom card development, you're now equipped to create innovative and engaging experiences for HubSpot users, opening up endless possibilities for extending HubSpot's functionality. 

This guide only scratches the surface of what's possible, so keep exploring and building!

Check out HubSpot sample cards for inspiration and don’t forget to test your custom cards thoroughly before deploying them

If you have any questions or need further assistance, feel free to reach out to us for support. Happy coding!

 

Share on:

Our blog newsletter

Subscribe to our blog and immerse yourself in a world full of exciting information and groundbreaking insights!
100% free of charge. You can unsubscribe at any time.

Free first consultation

Let's start the path to your success together and arrange your free initial consultation now!