Can UI Code Read Like the UI Itself?
Imagine a simple interaction: Click โ animate button โ fetch users โ render results โ highlight the first result. The user experiences these steps in a clear sequence. But does the code describe that sequence just as clearly? Can a single state hold not just a final value, but the transition like the button's animation that leads to it?
React
Here is one way to implement this interaction in React without using an animation library:
import { useState } from " react " ;
export default function UserSearch () {
const [ users , setUsers ] = useState ([]);
const [ isSearching , setIsSearching ] = useState ( false );
const [ isAnimating , setIsAnimating ] = useState ( false );
const [ highlightFirst , setHighlightFirst ] = useState ( false );
async function handleSearch () {
if ( isSearching ) return ;
setIsSearching ( true );
setUsers ([]);
setHighlightFirst ( false );
// Animate the button.
setIsAnimating ( true );
// Fetch users.
const response = await fetch ( " https://targetjs.io/api/randomUsers " );
const newUsers = await response . json ();
setIsAnimating ( false );
// Render the results.
setUsers ( newUsers );
// Wait for the new UI to appear.
await new Promise ( resolve => {
setTimeout ( resolve , 150 );
});
// Highlight the first result.
setHighlightFirst ( true );
setIsSearching ( false );
}
return (
< div className = "search" >
< button
className = { isAnimating ? " search-button active " : " search-button " }
disabled = { isSearching }
onClick = { handleSearch }
>
{ isSearching ? " Searching... " : " Search " }
</ button >
< div className = "users" >
{ users . map (( user , index ) => (
< div
className = { index === 0 && highlightFirst ? " user highlighted " : " user " }
key = { user . email }
>
< strong > { user . name } </ strong >
< span > { user . email } </ span >
</ div >
)) }
</ div >
</ div >
);
}
.search-button {
width : 220px ;
height : 60px ;
border : 0 ;
border-radius : 10px ;
cursor : pointer ;
transition : transform 300ms , background-color 300ms ;
}
.search-button.active {
transform : scale ( 1.15 );
background-color : #ffe8ec ;
}
.users {
display : grid ;
gap : 10px ;
margin-top : 20px ;
}
.user {
width : 340px ;
padding : 14px ;
border-radius : 10px ;
background : #fafafa ;
box-shadow : 0 6px 16px rgba ( 0 , 0 , 0 , 0.08 );
transition : transform 200ms , background-color 200ms ;
}
.user span {
display : block ;
margin-top : 5px ;
opacity : 0.7 ;
}
.user.highlighted {
transform : scale ( 1.04 );
background-color : #fff1a8 ;
}
The code works, but the interaction is represented through several different mechanisms:
- State variables control animation and rendering.
- Class names connect JavaScript to CSS.
- Promises and timers control sequencing.
- Conditional rendering applies the final highlight.
The sequence is present, but understanding it requires following logic distributed across the event handler, JSX, state variables, and CSS.
TargetJS
Here is the same interaction in TargetJS:
import { App } from " targetj " ;
App ({
containerOverflowMode : " always ",
searchButton : {
element : " button ",
type : " button ",
width : 220 ,
height : 60 ,
lineHeight : 60 ,
border : 0 ,
borderRadius : 10 ,
cursor : " pointer ",
textAlign : " center ",
backgroundColor : " #f5f5f5 ",
html : " Search ",
onClick () {
this . setTarget ( " html " , " Searching... " );
this . setTarget ( " scale " , {
value : [ 1 , 1.15 , 1 ],
steps : 12 ,
});
this . setTarget ( " backgroundColor " , {
value : [ " #ffe8ec " , " #f5f5f5 " ],
steps : 12 ,
});
const users = this . parent . getChild ( " users " );
users . activateTarget ( " search " , {
reset : true ,
});
},
},
users : {
gap : 10 ,
marginTop : 20 ,
containerOverflowMode : " always ",
search : {
active : false ,
value () {
this . removeChildren ();
},
},
fetch$ $ : " https://targetjs.io/api/randomUsers " ,
addChildren$ $ : {
cycles () {
return this . val ( " fetch " ). length ;
},
value ( i ) {
const user = this . val ( " fetch " )[ i ];
return {
width : 340 ,
padding : 14 ,
borderRadius : 10 ,
backgroundColor : " #fafafa ",
boxShadow : " 0 6px 16px rgba(0, 0, 0, 0.08) ",
containerOverflowMode : " always ",
scale : {
value : [ 0.96 , 1 ],
steps : 10 ,
},
userName : {
marginTop : 5 ,
html : user . name ,
fontWeight : 700 ,
},
userEmail : {
marginTop : 5 ,
html : user . email ,
opacity : 0.7 ,
},
};
},
},
relabelButton$ $ () {
const button = this . parent . getChild ( " searchButton " );
button . setTarget ( " html " , " Search " );
},
pause$ $ : {
interval : 150 ,
},
highlightFirst$ $ () {
const firstUser = this . getChild ( 0 );
if ( firstUser ) {
firstUser . setTarget ( " scale " , {
value : [ 1 , 1.04 , 1 ],
steps : 10 ,
});
firstUser . setTarget ( " backgroundColor " , {
value : " #fff1a8 ",
steps : 10 ,
});
}
},
},
}). mount ( " #app " );
The significant part is the order of the targets: search โ fetch$$ โ addChildren$$ โ relabelButton$$ โ pause$$ โ highlightFirst$$. That is also the order of the user experience mentioned above. The $$ suffix means that a target waits for the preceding target chain to complete before it runs.
The Comparison
In the React version, the workflow is coordinated through:
- an event handler,
- several pieces of state,
- conditional class names,
- JSX,
- CSS transitions,
- a promise,
- and a timer.
In the TargetJS version, the same workflow is represented directly by the order of its targets: remove โ fetch โ add โ pause โ highlight. The code reads more like the interaction itself.
TargetJS explores a model in which state is not only a final value. State can also describe a destination approached through a transition. Instead of only:
State โ Render
the model becomes:
State โ Transition โ Render
Instead of reconstructing the UI sequence from several different parts of the application, the sequence can be read from top to bottom.
Ready to see to learn more?
๐ Visit: GitHub Repo
๐ Site: targetjs.io
Comments
No comments yet. Start the discussion.